/* * Copyright 2010-2015 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 java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import icy.gui.frame.progress.FailedAnnounceFrame; import icy.gui.frame.progress.FileFrame; import icy.gui.menu.ApplicationMenu; import icy.image.IcyBufferedImage; import icy.image.IcyBufferedImageUtil; import icy.image.colormodel.IcyColorModel; import icy.image.lut.LUT; import icy.main.Icy; import icy.painter.Overlay; import icy.preferences.GeneralPreferences; import icy.roi.ROI; 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 loci.common.services.ServiceException; import loci.formats.FormatException; import loci.formats.IFormatWriter; import loci.formats.UnknownFormatException; import loci.formats.meta.MetadataRetrieve; 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; import ome.xml.meta.OMEXMLMetadata; /** * 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 loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ, int sizeT, DataType dataType) throws ServiceException { return (loci.formats.ome.OMEXMLMetadata) 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 loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ, int sizeT, int dataType, boolean signedDataType) throws ServiceException { return (loci.formats.ome.OMEXMLMetadata) 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 loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, DataType dataType) throws ServiceException { return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC, 1, 1, dataType, false); } /** * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, DataType, boolean)} instead */ @Deprecated public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int dataType, boolean signedDataType) throws ServiceException { return (loci.formats.ome.OMEXMLMetadata) 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 the specified ImageFileFormat * from the specified image description.<br> * That means this file format is able to save the data described by the returned {@link IcyColorModel} without any * loss * or conversion.<br> * * @param imageFileFormat * Image file format we want to test compatibility * @param numChannel * number of channel of the image * @param dataType * image data type */ public static IcyColorModel getCompatibleColorModel(ImageFileFormat imageFileFormat, int numChannel, DataType dataType) { final DataType outDataType; final int outNumChannel; switch (imageFileFormat) { default: case TIFF: // TIFF supports all formats outDataType = dataType; outNumChannel = numChannel; break; case PNG: // PNG only supports byte data type (short is not really valid) if (dataType.getSize() > 1) outDataType = DataType.UBYTE; else outDataType = dataType; // PNG supports a maximum of 4 channels outNumChannel = Math.min(numChannel, 4); break; case AVI: case JPG: // 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; } break; } return IcyColorModel.createInstance(outNumChannel, outDataType); } /** * Return the closest compatible {@link IcyColorModel} supported by the specified image file format * from the specified {@link IcyColorModel}.<br> * That means this image file format supports saving data described by the returned {@link IcyColorModel} without * any loss or conversion. * * @param imageFileFormat * Image file format we want to test compatibility * @param colorModel * the colorModel describing data / image format */ public static IcyColorModel getCompatibleColorModel(ImageFileFormat imageFileFormat, IcyColorModel colorModel) { return getCompatibleColorModel(imageFileFormat, colorModel.getNumComponents(), colorModel.getDataType_()); } /** * Return true if the specified image file format is compatible with the image description.<br> * That means this image file format supports saving data without any loss or conversion. * * @param imageFileFormat * Image file format we want to test compatibility * @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(ImageFileFormat imageFileFormat, int numChannel, boolean alpha, DataType dataType) { return isCompatible(imageFileFormat, IcyColorModel.createInstance(numChannel, dataType)); } /** * Return true if the specified image file format is compatible with the given {@link IcyColorModel}. <br> * That means this image file format supports saving data described by the returned {@link IcyColorModel} 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(ImageFileFormat imageFileFormat, IcyColorModel colorModel) { return colorModel.isCompatible(getCompatibleColorModel(imageFileFormat, colorModel)); } /** * Return true if the specified image file format is compatible to save the given Sequence.<br> * That means this image file format supports saving all original data (3D/4D/5D) without any loss or conversion. */ public static boolean isCompatible(ImageFileFormat imageFileFormat, Sequence sequence) { final boolean multiZ = sequence.getSizeZ() > 1; final boolean multiT = sequence.getSizeT() > 1; switch (imageFileFormat) { case JPG: case PNG: // JPG and PNG: no support for time sequence or 3D image if ((multiZ) || (multiT)) return false; break; case AVI: // AVI: not support for 3D image if (multiZ) return false; break; } return isCompatible(imageFileFormat, sequence.getColorModel()); } /** * Return the separate channel flag from specified image file format and color space */ private static boolean getSeparateChannelFlag(ImageFileFormat imageFileFormat, int numChannel, DataType dataType) { // only if we have more than 1 channel if (numChannel > 1) { // only TIFF writer support it: better to not separate channel for RGB images if (imageFileFormat.equals(ImageFileFormat.TIFF)) return (numChannel != 3) || (dataType.getSize() > 1); } // others writers does not support separated channel return false; } /** * Return the separate channel flag from specified image file format and color space */ private static boolean getSeparateChannelFlag(ImageFileFormat imageFileFormat, IcyColorModel colorModel) { return getSeparateChannelFlag(imageFileFormat, colorModel.getNumComponents(), colorModel.getDataType_()); } /** * 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) { return getCompatibleColorModel(getImageFileFormat(writer, ImageFileFormat.TIFF), numChannel, dataType); } /** * 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 given {@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 true if the specified writer is compatible to save the given Sequence.<br> * That means the writer is able to save all original data (3D/4D/5D) without any loss or conversion. */ public static boolean isCompatible(IFormatWriter writer, Sequence sequence) { return isCompatible(getImageFileFormat(writer, ImageFileFormat.TIFF), sequence); } /** * Return the separate channel flag from specified writer and color space */ private static boolean getSeparateChannelFlag(IFormatWriter writer, int numChannel, DataType dataType) { return getSeparateChannelFlag(getImageFileFormat(writer, ImageFileFormat.TIFF), numChannel, dataType); } /** * 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, 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, 15, multipleFiles, showProgress); } /** * @deprecated Use {@link #save(Sequence, File, 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); } /** * @deprecated Use {@link #save(Sequence, File, int, boolean, boolean)} instead. */ @Deprecated 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, 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<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 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 fps, boolean multipleFile, boolean showProgress) { save(null, sequence, file, fps, multipleFile, showProgress, true); } /** * @deprecated Use {@link #save(IFormatWriter, Sequence, File, 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, fps, multipleFile, showProgress, true); } /** * @deprecated Use {@link #save(IFormatWriter, Sequence, File, 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, boolean addToRecent) { save(formatWriter, sequence, file, fps, multipleFile, showProgress, addToRecent); } /** * 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).<br> * @param fps * frame rate for AVI sequence save * @param multipleFile * flag to indicate if images are saved in separate file.<br> * When multiple file is enabled the <code>file</code> parameter is considerer as a folder if it doens't have * any extension * @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 fps, boolean multipleFile, boolean showProgress, boolean addToRecent) { final String filePath = FileUtil.cleanPath(FileUtil.getGenericPath(file.getAbsolutePath())); final int sizeT = sequence.getSizeT(); final int sizeZ = sequence.getSizeZ(); 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); } final IFormatWriter writer; final Sequence savedSequence; // get the writer 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); // need multiple files ? if ((numImages > 1) && multipleFile) { // 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); // assume that is the saved sequence (used for metadata) savedSequence = sequence; for (int t = 0; t < sizeT; t++) { for (int z = 0; z < sizeZ; z++) { String filename = filePathWithoutExt; if (sizeT > 1) filename += "_t" + decimalFormat.format(t); if (sizeZ > 1) filename += "_z" + decimalFormat.format(z); filename += fileExt; // save as single image file save(writer, sequence, filename, t, z, fps, saveFrame); } } // add as one item to recent file list if (mainMenu != null) mainMenu.addRecentFile(fileBaseDirectory); } else { final ImageFileFormat iff = getImageFileFormat(writer, ImageFileFormat.TIFF); final String fileExt = FileUtil.getFileExtension(filePath, false); // 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(filePath, false)); // save whole sequence into a single file savedSequence = save(writer, sequence, fixedFilePath, -1, -1, fps, saveFrame); // we set filename on actual saved Sequence savedSequence.setFilename(filePath); // add as one item to recent file list if (mainMenu != null) mainMenu.addRecentFile(fixedFilePath); } // Sequence persistence enabled --> save XML if (GeneralPreferences.getSequencePersistence()) savedSequence.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, boolean separateChannel, DataType dataType, File file, boolean force) throws FormatException, IOException { final String filePath = FileUtil.cleanPath(FileUtil.getGenericPath(file.getAbsolutePath())); if (FileUtil.exists(filePath)) { // forced ? first delete the file else LOCI won't save it if (force) FileUtil.delete(filePath, true); else throw new IOException("File already exists"); } // ensure parent directory exist FileUtil.ensureParentDirExist(filePath); final IFormatWriter writer; final boolean separateCh; if (formatWriter == null) { // get the writer writer = getWriter(FileUtil.getFileExtension(filePath, false), ImageFileFormat.TIFF); // prepare the metadata try { separateCh = getSeparateChannelFlag(writer, numChannel, dataType); writer.setMetadataRetrieve((MetadataRetrieve) MetaDataUtil.generateMetaData(width, height, numChannel, dataType, separateCh)); } catch (ServiceException e) { System.err.println("Saver.saveImage(...) error :"); IcyExceptionHandler.showErrorMessage(e, true); } } else { // ready to use writer (metadata already prepared) writer = formatWriter; separateCh = separateChannel; } // we never interleaved data even if some image viewer need it to correctly read image (win XP viewer) writer.setInterleaved(false); writer.setId(filePath); writer.setSeries(0); // usually give better save performance writer.setWriteSequentially(true); try { // separated channel data if (separateChannel) { final int pitch = width * height * dataType.getSize(); final byte[] dataChannel = new byte[pitch]; int offset = 0; for (int c = 0; c < numChannel; c++) { System.arraycopy(data, offset, dataChannel, 0, pitch); writer.saveBytes(c, dataChannel); offset += pitch; } } else // save all data at once 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, false, 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); final boolean separateChannel = getSeparateChannelFlag(writer, image.getIcyColorModel()); try { writer.setMetadataRetrieve((MetadataRetrieve) MetaDataUtil.generateMetaData(image, separateChannel)); } 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 saveImage(writer, image.getRawData(littleEndian), image.getSizeX(), image.getSizeY(), image.getSizeC(), separateChannel, image.getDataType_(), file, force); } /** * Save the specified sequence in the specified file using the given writer.<br> * If posT or/and posZ are defined then only a sub part of the original Sequence is saved. * * @param writer * writer used to save sequence (define the image format, cannot be <code>null</code> at this point) * @param sequence * sequence to save * @param filePath * file name where we want to save sequence * @param posT * frame index to save (-1 to save all frame from input sequence) * @param posZ * slice index to save (-1 to save all slice from input sequence) * @param fps * frame rate for AVI writer * @param saveFrame * progress frame for save operation (can be null) * @return Actual saved Sequence (can be different from input one if conversion was needed) * @throws ServiceException * @throws IOException * @throws FormatException */ private static Sequence save(IFormatWriter writer, Sequence sequence, String filePath, int posT, int posZ, int fps, FileFrame saveFrame) throws ServiceException, FormatException, IOException { // 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); // } // } final File file = new 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 ImageFileFormat saveFormat = getImageFileFormat(writer, ImageFileFormat.TIFF); final int sizeT = sequence.getSizeT(); final int sizeZ = sequence.getSizeZ(); final int adjZ, adjT; final int tMin, tMax; final int zMin, zMax; // adjust posT and posZ depending the writer support switch (saveFormat) { default: case TIFF: // no restriction for TIFF adjZ = posZ; adjT = posT; break; case AVI: // AVI: always save single slice adjZ = (posZ < 0) ? sizeZ / 2 : posZ; adjT = posT; break; case JPG: case PNG: // JPG or PNG: always save single image adjZ = (posZ < 0) ? sizeZ / 2 : posZ; adjT = (posT < 0) ? sizeT / 2 : posT; break; } // convert Sequence in good format for specified writer final Sequence compatibleSequence = getCompatibleSequenceForWriter(writer, sequence, adjT, adjZ); // get channel separation flag final boolean separateChannel = getSeparateChannelFlag(saveFormat, compatibleSequence.getColorModel()); // prepare metadata final OMEXMLMetadata metadata = MetaDataUtil.generateMetaData(compatibleSequence, separateChannel); // clean unwanted planes MetaDataUtil.keepPlanes(metadata, 0, adjT, adjZ, -1); if (adjT < 0) { // all frame tMin = 0; tMax = sizeT - 1; } else { // single frame tMin = tMax = adjT; MetaDataUtil.setSizeT(metadata, 0, 1); } if (adjZ < 0) { // all slice zMin = 0; zMax = sizeZ - 1; } else { // single slice zMin = zMax = adjZ; MetaDataUtil.setSizeZ(metadata, 0, 1); } // specific to TIFF writer if (writer instanceof TiffWriter) { // > 2GB --> use big tiff (important to do it before setId(..) call) if (MetaDataUtil.getDataSize(metadata, 0, 0) > 2000000000L) ((TiffWriter) writer).setBigTiff(true); } // set settings writer.setFramesPerSecond(fps); // generate metadata writer.setMetadataRetrieve((MetadataRetrieve) metadata); // no interleave (XP default viewer want interleaved channel to correctly read image) writer.setInterleaved(false); // set id writer.setId(filePath); // init writer.setSeries(0); // usually give better save performance writer.setWriteSequentially(true); final int sizeC = compatibleSequence.getSizeC(); // 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++) { // interrupt process (partial save) if ((saveFrame != null) && saveFrame.isCancelRequested()) return compatibleSequence; final IcyBufferedImage image = compatibleSequence.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); writer.saveBytes(imageIndex, data); } imageIndex++; } if (saveFrame != null) saveFrame.incPosition(); } } } finally { // always close writer after a file has been saved writer.close(); } return compatibleSequence; } /** * Returns a compatible Sequence representing the input sequence so it can be saved with the specified writer.<br> * If the writer support the input sequence then the input sequence is directly returned. * * @param writer * writer used to save sequence (define the image format, cannot be <code>null</code>) * @param sequence * sequence to save * @param posT * frame index to keep (-1 for all frame) * @param posZ * slice index to keep (-1 for all slice) * @return the compatible sequence for given Writer */ public static Sequence getCompatibleSequenceForWriter(IFormatWriter writer, Sequence sequence, int posT, int posZ) { final int sizeC = sequence.getSizeC(); final DataType dataType = sequence.getDataType_(); final boolean needConvert; final ImageFileFormat imageFormat = getImageFileFormat(writer, ImageFileFormat.TIFF); // adjust posT and posZ depending the writer support switch (imageFormat) { default: // assume TIFF needConvert = false; break; case AVI: case JPG: // JPG, AVI: only supports byte data type and Gray/RGB images needConvert = (dataType.getSize() > 1) || (sizeC == 2) || (sizeC > 3); break; case PNG: // PNG: support byte data type with a maximum of 4 channels needConvert = (dataType.getSize() > 1) || (sizeC > 4); break; } // no conversion needed if (!needConvert) return sequence; final int sizeT = sequence.getSizeT(); final int sizeZ = sequence.getSizeZ(); final int tMin, tMax; final int zMin, zMax; if (posT < 0) { // all frame tMin = 0; tMax = sizeT - 1; } else // single frame tMin = tMax = posT; if (posZ < 0) { // all slice zMin = 0; zMax = sizeZ - 1; } else // single slice zMin = zMax = posZ; // wanted image type final int imageType = (sizeC > 1) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_BYTE_GRAY; // image receiver final BufferedImage imgOut = new BufferedImage(sequence.getSizeX(), sequence.getSizeY(), imageType); // conversion LUT (use default sequence one) final LUT lut = sequence.getDefaultLUT(); // create compatible sequence final Sequence result = new Sequence(OMEUtil.createOMEMetadata(sequence.getMetadata())); result.beginUpdate(); try { for (int t = tMin; t <= tMax; t++) for (int z = zMin; z <= zMax; z++) result.setImage(t, z, IcyBufferedImageUtil.toBufferedImage(sequence.getImage(t, z), imgOut, lut)); // preserve ROI and overlays (for XML metadata preservation) for (ROI roi : sequence.getROIs()) result.addROI(roi); for (Overlay overlay : sequence.getOverlays()) result.addOverlay(overlay); // rename channels and set final name switch (imageType) { default: case BufferedImage.TYPE_INT_RGB: result.setChannelName(0, "red"); result.setChannelName(1, "green"); result.setChannelName(2, "blue"); break; case BufferedImage.TYPE_BYTE_GRAY: result.setChannelName(0, "gray"); break; } result.setName(sequence.getName() + " (" + imageFormat + ")"); } finally { result.endUpdate(); } return result; } }