001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.file;
020
021import icy.gui.frame.progress.FailedAnnounceFrame;
022import icy.gui.frame.progress.FileFrame;
023import icy.gui.menu.ApplicationMenu;
024import icy.image.IcyBufferedImage;
025import icy.image.IcyBufferedImageUtil;
026import icy.image.colormodel.IcyColorModel;
027import icy.image.lut.LUT;
028import icy.main.Icy;
029import icy.painter.Overlay;
030import icy.preferences.GeneralPreferences;
031import icy.roi.ROI;
032import icy.sequence.MetaDataUtil;
033import icy.sequence.Sequence;
034import icy.system.IcyExceptionHandler;
035import icy.type.DataType;
036import icy.util.OMEUtil;
037import icy.util.StringUtil;
038
039import java.awt.image.BufferedImage;
040import java.io.File;
041import java.io.IOException;
042import java.text.DecimalFormat;
043
044import loci.common.services.ServiceException;
045import loci.formats.FormatException;
046import loci.formats.IFormatWriter;
047import loci.formats.UnknownFormatException;
048import loci.formats.meta.MetadataRetrieve;
049import loci.formats.out.APNGWriter;
050import loci.formats.out.AVIWriter;
051import loci.formats.out.JPEG2000Writer;
052import loci.formats.out.JPEGWriter;
053import loci.formats.out.OMETiffWriter;
054import loci.formats.out.TiffWriter;
055import ome.xml.meta.OMEXMLMetadata;
056
057/**
058 * Sequence / Image saver class.<br>
059 * <br>
060 * Supported save format are the following : TIFF (preferred), PNG, JPG and AVI.
061 * When sequence is saved as multiple file the following naming convention is used :<br>
062 * <code>filename-tttt-zzzz</code>
063 * 
064 * @author Stephane & Fab
065 */
066public class Saver
067{
068    /**
069     * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, int, int, DataType, boolean)} instead
070     */
071    @Deprecated
072    public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ,
073            int sizeT, DataType dataType) throws ServiceException
074    {
075        return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC, sizeZ, sizeT, dataType,
076                false);
077    }
078
079    /**
080     * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, int, int, DataType, boolean)} instead
081     */
082    @Deprecated
083    public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ,
084            int sizeT, int dataType, boolean signedDataType) throws ServiceException
085    {
086        return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC, sizeZ, sizeT,
087                DataType.getDataType(dataType, signedDataType), false);
088    }
089
090    /**
091     * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, DataType, boolean)} instead
092     */
093    @Deprecated
094    public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, DataType dataType)
095            throws ServiceException
096    {
097        return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC, 1, 1, dataType, false);
098    }
099
100    /**
101     * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, DataType, boolean)} instead
102     */
103    @Deprecated
104    public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int dataType,
105            boolean signedDataType) throws ServiceException
106    {
107        return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC,
108                DataType.getDataType(dataType, signedDataType), false);
109    }
110
111    /**
112     * Returns the {@link ImageFileFormat} corresponding to specified {@link IFormatWriter}.<br>
113     * <code>defaultValue</code> is returned if no matching format is found.
114     */
115    public static ImageFileFormat getImageFileFormat(IFormatWriter writer, ImageFileFormat defaultValue)
116    {
117        if (writer instanceof TiffWriter)
118            return ImageFileFormat.TIFF;
119        if (writer instanceof APNGWriter)
120            return ImageFileFormat.PNG;
121        if (writer instanceof JPEGWriter)
122            return ImageFileFormat.JPG;
123        if (writer instanceof JPEG2000Writer)
124            return ImageFileFormat.JPG;
125        if (writer instanceof AVIWriter)
126            return ImageFileFormat.AVI;
127
128        return defaultValue;
129    }
130
131    /**
132     * @deprecated Use {@link #getImageFileFormat(IFormatWriter, ImageFileFormat)} instead.
133     */
134    @Deprecated
135    public static FileFormat getFileFormat(IFormatWriter writer, FileFormat defaultValue)
136    {
137        return getImageFileFormat(writer, ImageFileFormat.getFormat(defaultValue)).toFileFormat();
138    }
139
140    /**
141     * Return the writer to use for the specified ImageFileFormat.<br>
142     * <br>
143     * The following writer are currently supported :<br>
144     * <code>OMETiffWriter</code> : TIFF image file (default)<br>
145     * <code>APNGWriter</code> : PNG image file<br>
146     * <code>JPEGWriter</code> : JPG image file<br>
147     * <code>AVIWriter</code> : AVI video file<br>
148     * 
149     * @param format
150     *        {@link ImageFileFormat} we want to retrieve the saver.<br>
151     *        Accepted values:<br>
152     *        {@link ImageFileFormat#TIFF}<br>
153     *        {@link ImageFileFormat#PNG}<br>
154     *        {@link ImageFileFormat#JPG}<br>
155     *        {@link ImageFileFormat#AVI}<br>
156     *        null
157     */
158    public static IFormatWriter getWriter(ImageFileFormat format)
159    {
160        final IFormatWriter result;
161
162        switch (format)
163        {
164            case PNG:
165                result = new APNGWriter();
166                break;
167
168            case JPG:
169                result = new JPEGWriter();
170                break;
171
172            case AVI:
173                result = new AVIWriter();
174                break;
175
176            default:
177                result = new OMETiffWriter();
178                // this way we are sure the TIF saver is always compressing
179                try
180                {
181                    result.setCompression("LZW");
182                }
183                catch (FormatException e)
184                {
185                    // no compression
186                }
187                break;
188        }
189
190        return result;
191    }
192
193    /**
194     * @deprecated Use {@link #getWriter(ImageFileFormat)} instead.
195     */
196    @Deprecated
197    public static IFormatWriter getWriter(FileFormat fileFormat)
198    {
199        return getWriter(ImageFileFormat.getFormat(fileFormat));
200    }
201
202    /**
203     * Return the writer to use for the specified filename extension.<br>
204     * <br>
205     * The following writer are currently supported :<br>
206     * <code>OMETiffWriter</code> : TIFF image file (default)<br>
207     * <code>APNGWriter</code> : PNG image file<br>
208     * <code>JPEGWriter</code> : JPG image file<br>
209     * <code>AVIWriter</code> : AVI video file<br>
210     * 
211     * @param ext
212     *        Extension we want to retrieve the corresponding image writer.
213     * @param defaultFormat
214     *        default {@link ImageFileFormat} to use if <code>ext</code> is not recognized.<br>
215     *        Accepted values:<br>
216     *        {@link ImageFileFormat#TIFF}<br>
217     *        {@link ImageFileFormat#PNG}<br>
218     *        {@link ImageFileFormat#JPG}<br>
219     *        {@link ImageFileFormat#AVI}<br>
220     *        null
221     */
222    public static IFormatWriter getWriter(String ext, ImageFileFormat defaultFormat)
223    {
224        return getWriter(ImageFileFormat.getWriteFormat(ext, defaultFormat));
225    }
226
227    /**
228     * @deprecated Use {@link #getWriter(String, ImageFileFormat)} instead.
229     */
230    @Deprecated
231    public static IFormatWriter getWriter(String ext, FileFormat defaultFormat)
232    {
233        return getWriter(ext, ImageFileFormat.getFormat(defaultFormat));
234    }
235
236    /**
237     * @deprecated Use {@link #getWriter(String, FileFormat)} instead.
238     */
239    @Deprecated
240    public static IFormatWriter getWriter(String ext)
241    {
242        return getWriter(ext, ImageFileFormat.TIFF);
243    }
244
245    /**
246     * Return the writer to use for the specified file.<br>
247     * <br>
248     * The following writer are currently supported :<br>
249     * <code>OMETiffWriter</code> : TIFF image file (default)<br>
250     * <code>APNGWriter</code> : PNG image file<br>
251     * <code>JPEGWriter</code> : JPG image file<br>
252     * <code>AVIWriter</code> : AVI video file<br>
253     * 
254     * @param file
255     *        File we want to retrieve the corresponding image writer.
256     * @param defaultFormat
257     *        default {@link ImageFileFormat} to use if <code>file</code> is not recognized.<br>
258     *        Accepted values:<br>
259     *        {@link ImageFileFormat#TIFF}<br>
260     *        {@link ImageFileFormat#PNG}<br>
261     *        {@link ImageFileFormat#JPG}<br>
262     *        {@link ImageFileFormat#AVI}<br>
263     *        null
264     */
265    public static IFormatWriter getWriter(File file, ImageFileFormat defaultFormat)
266    {
267        return getWriter(FileUtil.getFileExtension(file.getName(), false), defaultFormat);
268    }
269
270    /**
271     * @deprecated Use {@link #getWriter(File, ImageFileFormat)} instead.
272     */
273    @Deprecated
274    public static IFormatWriter getWriter(File file, FileFormat defaultFormat)
275    {
276        return getWriter(file, ImageFileFormat.getFormat(defaultFormat));
277    }
278
279    /**
280     * @deprecated Use {@link #getWriter(File, FileFormat)} instead.
281     */
282    @Deprecated
283    public static IFormatWriter getWriter(File file)
284    {
285        return getWriter(file, ImageFileFormat.TIFF);
286    }
287
288    /**
289     * Return the closest compatible {@link IcyColorModel} supported by the specified ImageFileFormat
290     * from the specified image description.<br>
291     * That means this file format is able to save the data described by the returned {@link IcyColorModel} without any
292     * loss
293     * or conversion.<br>
294     * 
295     * @param imageFileFormat
296     *        Image file format we want to test compatibility
297     * @param numChannel
298     *        number of channel of the image
299     * @param dataType
300     *        image data type
301     */
302    public static IcyColorModel getCompatibleColorModel(ImageFileFormat imageFileFormat, int numChannel,
303            DataType dataType)
304    {
305        final DataType outDataType;
306        final int outNumChannel;
307
308        switch (imageFileFormat)
309        {
310            default:
311            case TIFF:
312                // TIFF supports all formats
313                outDataType = dataType;
314                outNumChannel = numChannel;
315                break;
316
317            case PNG:
318                // PNG only supports byte data type (short is not really valid)
319                if (dataType.getSize() > 1)
320                    outDataType = DataType.UBYTE;
321                else
322                    outDataType = dataType;
323
324                // PNG supports a maximum of 4 channels
325                outNumChannel = Math.min(numChannel, 4);
326                break;
327
328            case AVI:
329            case JPG:
330                // JPG, AVI, default only supports byte data type
331                if (dataType.getSize() > 1)
332                    outDataType = DataType.UBYTE;
333                else
334                    outDataType = dataType;
335
336                // 3 channels at max
337                if (numChannel > 3)
338                    outNumChannel = 3;
339                else
340                {
341                    // special case of 2 channels
342                    if (numChannel == 2)
343                        // convert to RGB
344                        outNumChannel = 3;
345                    else
346                        outNumChannel = numChannel;
347                }
348                break;
349        }
350
351        return IcyColorModel.createInstance(outNumChannel, outDataType);
352    }
353
354    /**
355     * Return the closest compatible {@link IcyColorModel} supported by the specified image file format
356     * from the specified {@link IcyColorModel}.<br>
357     * That means this image file format supports saving data described by the returned {@link IcyColorModel} without
358     * any loss or conversion.
359     * 
360     * @param imageFileFormat
361     *        Image file format we want to test compatibility
362     * @param colorModel
363     *        the colorModel describing data / image format
364     */
365    public static IcyColorModel getCompatibleColorModel(ImageFileFormat imageFileFormat, IcyColorModel colorModel)
366    {
367        return getCompatibleColorModel(imageFileFormat, colorModel.getNumComponents(), colorModel.getDataType_());
368    }
369
370    /**
371     * Return true if the specified image file format is compatible with the image description.<br>
372     * That means this image file format supports saving data without any loss or conversion.
373     * 
374     * @param imageFileFormat
375     *        Image file format we want to test compatibility
376     * @param numChannel
377     *        number of channel of the image
378     * @param alpha
379     *        true if the image has an alpha channel
380     * @param dataType
381     *        image data type
382     */
383    public static boolean isCompatible(ImageFileFormat imageFileFormat, int numChannel, boolean alpha,
384            DataType dataType)
385    {
386        return isCompatible(imageFileFormat, IcyColorModel.createInstance(numChannel, dataType));
387    }
388
389    /**
390     * Return true if the specified image file format is compatible with the given {@link IcyColorModel}. <br>
391     * That means this image file format supports saving data described by the returned {@link IcyColorModel} without
392     * any loss or conversion.<br>
393     * The color map data are never preserved, they are always restored to their default.<br>
394     */
395    public static boolean isCompatible(ImageFileFormat imageFileFormat, IcyColorModel colorModel)
396    {
397        return colorModel.isCompatible(getCompatibleColorModel(imageFileFormat, colorModel));
398    }
399
400    /**
401     * Return true if the specified image file format is compatible to save the given Sequence.<br>
402     * That means this image file format supports saving all original data (3D/4D/5D) without any loss or conversion.
403     */
404    public static boolean isCompatible(ImageFileFormat imageFileFormat, Sequence sequence)
405    {
406        final boolean multiZ = sequence.getSizeZ() > 1;
407        final boolean multiT = sequence.getSizeT() > 1;
408
409        switch (imageFileFormat)
410        {
411            case JPG:
412            case PNG:
413                // JPG and PNG: no support for time sequence or 3D image
414                if ((multiZ) || (multiT))
415                    return false;
416                break;
417
418            case AVI:
419                // AVI: not support for 3D image
420                if (multiZ)
421                    return false;
422                break;
423        }
424
425        return isCompatible(imageFileFormat, sequence.getColorModel());
426    }
427
428    /**
429     * Return the separate channel flag from specified image file format and color space
430     */
431    private static boolean getSeparateChannelFlag(ImageFileFormat imageFileFormat, int numChannel, DataType dataType)
432    {
433        // only if we have more than 1 channel
434        if (numChannel > 1)
435        {
436            // only TIFF writer support it: better to not separate channel for RGB images
437            if (imageFileFormat.equals(ImageFileFormat.TIFF))
438                return (numChannel != 3) || (dataType.getSize() > 1);
439        }
440
441        // others writers does not support separated channel
442        return false;
443    }
444
445    /**
446     * Return the separate channel flag from specified image file format and color space
447     */
448    private static boolean getSeparateChannelFlag(ImageFileFormat imageFileFormat, IcyColorModel colorModel)
449    {
450        return getSeparateChannelFlag(imageFileFormat, colorModel.getNumComponents(), colorModel.getDataType_());
451    }
452
453    /**
454     * Return the closest compatible {@link IcyColorModel} supported by writer
455     * from the specified image description.<br>
456     * That means the writer is able to save the data described by the returned {@link IcyColorModel} without any loss
457     * or conversion.<br>
458     * 
459     * @param writer
460     *        IFormatWriter we want to test compatibility
461     * @param numChannel
462     *        number of channel of the image
463     * @param dataType
464     *        image data type
465     */
466    public static IcyColorModel getCompatibleColorModel(IFormatWriter writer, int numChannel, DataType dataType)
467    {
468        return getCompatibleColorModel(getImageFileFormat(writer, ImageFileFormat.TIFF), numChannel, dataType);
469    }
470
471    /**
472     * Return the closest compatible {@link IcyColorModel} supported by writer
473     * from the specified {@link IcyColorModel}.<br>
474     * That means the writer is able to save the data described by the returned {@link IcyColorModel} without any loss
475     * or conversion.<br>
476     * 
477     * @param writer
478     *        IFormatWriter we want to test compatibility
479     * @param colorModel
480     *        the colorModel describing data / image format
481     */
482    public static IcyColorModel getCompatibleColorModel(IFormatWriter writer, IcyColorModel colorModel)
483    {
484        return getCompatibleColorModel(writer, colorModel.getNumComponents(), colorModel.getDataType_());
485    }
486
487    /**
488     * Return true if the specified writer is compatible with the image description.<br>
489     * That means the writer is able to save the data without any loss or conversion.<br>
490     * 
491     * @param numChannel
492     *        number of channel of the image
493     * @param alpha
494     *        true if the image has an alpha channel
495     * @param dataType
496     *        image data type
497     */
498    public static boolean isCompatible(IFormatWriter writer, int numChannel, boolean alpha, DataType dataType)
499    {
500        return isCompatible(writer, IcyColorModel.createInstance(numChannel, dataType));
501    }
502
503    /**
504     * Return true if the specified writer is compatible with the given {@link IcyColorModel}. <br>
505     * That means the writer is able to save the data described by the colorModel without any loss
506     * or conversion.<br>
507     * The color map data are never preserved, they are always restored to their default.<br>
508     */
509    public static boolean isCompatible(IFormatWriter writer, IcyColorModel colorModel)
510    {
511        return colorModel.isCompatible(getCompatibleColorModel(writer, colorModel));
512    }
513
514    /**
515     * Return true if the specified writer is compatible to save the given Sequence.<br>
516     * That means the writer is able to save all original data (3D/4D/5D) without any loss or conversion.
517     */
518    public static boolean isCompatible(IFormatWriter writer, Sequence sequence)
519    {
520        return isCompatible(getImageFileFormat(writer, ImageFileFormat.TIFF), sequence);
521    }
522
523    /**
524     * Return the separate channel flag from specified writer and color space
525     */
526    private static boolean getSeparateChannelFlag(IFormatWriter writer, int numChannel, DataType dataType)
527    {
528        return getSeparateChannelFlag(getImageFileFormat(writer, ImageFileFormat.TIFF), numChannel, dataType);
529    }
530
531    /**
532     * Return the separate channel flag from specified writer and color space
533     */
534    private static boolean getSeparateChannelFlag(IFormatWriter writer, IcyColorModel colorModel)
535    {
536        return getSeparateChannelFlag(writer, colorModel.getNumComponents(), colorModel.getDataType_());
537    }
538
539    /**
540     * Save the specified sequence in the specified file.<br>
541     * If sequence contains severals images then file is used as a directory<br>
542     * to store all single images.
543     * 
544     * @param sequence
545     *        sequence to save
546     * @param file
547     *        file where we want to save sequence
548     */
549    public static void save(Sequence sequence, File file)
550    {
551        save(sequence, file, 15, (sequence.getSizeZ() * sequence.getSizeT()) > 1, true);
552    }
553
554    /**
555     * @deprecated Use {@link #save(Sequence, File, boolean, boolean)} instead.
556     */
557    @Deprecated
558    public static void save(Sequence sequence, File file, boolean multipleFiles)
559    {
560        save(sequence, file, 0, sequence.getSizeZ() - 1, 0, sequence.getSizeT() - 1, 15, multipleFiles, true);
561    }
562
563    /**
564     * Save the specified sequence in the specified file.<br>
565     * When the sequence contains severals image the multiFile flag is used to indicate<br>
566     * if images are saved in severals files (file then specify a directory) or in a single file.
567     * 
568     * @param sequence
569     *        sequence to save
570     * @param file
571     *        file where we want to save sequence
572     * @param multipleFiles
573     *        flag to indicate if images are saved in separate file
574     * @param showProgress
575     *        show progress bar
576     */
577    public static void save(Sequence sequence, File file, boolean multipleFiles, boolean showProgress)
578    {
579        save(sequence, file, 15, multipleFiles, showProgress);
580    }
581
582    /**
583     * @deprecated Use {@link #save(Sequence, File, int, boolean, boolean)} instead.
584     */
585    @Deprecated
586    public static void save(Sequence sequence, File file, int zMin, int zMax, int tMin, int tMax, int fps,
587            boolean multipleFiles)
588    {
589        save(sequence, file, zMin, zMax, tMin, tMax, fps, multipleFiles, true);
590    }
591
592    /**
593     * @deprecated Use {@link #save(Sequence, File, int, boolean, boolean)} instead.
594     */
595    @Deprecated
596    public static void save(Sequence sequence, File file, int zMin, int zMax, int tMin, int tMax, int fps,
597            boolean multipleFile, boolean showProgress)
598    {
599        save(null, sequence, file, fps, multipleFile, showProgress, true);
600    }
601
602    /**
603     * Save the specified sequence in the specified file.<br>
604     * When the sequence contains severals image the multipleFile flag is used to indicate<br>
605     * if images are saved as separate files (file then specify a directory) or not.<br>
606     * zMin - zMax and tMin - tMax define the Z and T images range to save.<br>
607     * 
608     * @param sequence
609     *        sequence to save
610     * @param file
611     *        file where we want to save sequence
612     * @param fps
613     *        frame rate for AVI sequence save
614     * @param multipleFile
615     *        flag to indicate if images are saved in separate file
616     * @param showProgress
617     *        show progress bar
618     */
619    public static void save(Sequence sequence, File file, int fps, boolean multipleFile, boolean showProgress)
620    {
621        save(null, sequence, file, fps, multipleFile, showProgress, true);
622    }
623
624    /**
625     * @deprecated Use {@link #save(IFormatWriter, Sequence, File, int, boolean, boolean, boolean)} instead.
626     */
627    @Deprecated
628    public static void save(IFormatWriter formatWriter, Sequence sequence, File file, int zMin, int zMax, int tMin,
629            int tMax, int fps, boolean multipleFile, boolean showProgress)
630    {
631        save(formatWriter, sequence, file, fps, multipleFile, showProgress, true);
632    }
633
634    /**
635     * @deprecated Use {@link #save(IFormatWriter, Sequence, File, int, boolean, boolean, boolean)} instead.
636     */
637    @Deprecated
638    public static void save(IFormatWriter formatWriter, Sequence sequence, File file, int zMin, int zMax, int tMin,
639            int tMax, int fps, boolean multipleFile, boolean showProgress, boolean addToRecent)
640    {
641        save(formatWriter, sequence, file, fps, multipleFile, showProgress, addToRecent);
642    }
643
644    /**
645     * Save the specified sequence in the specified file.<br>
646     * When the sequence contains severals image the multipleFile flag is used to indicate
647     * if images are saved as separate files (file then specify a directory) or not.<br>
648     * <code>zMin</code> - <code>zMax</code> and <code>tMin</code> - <code>tMax</code> define the Z
649     * and T images range to save.<br>
650     * 
651     * @param formatWriter
652     *        writer used to save sequence (define the image format).<br>
653     *        If set to <code>null</code> then writer is determined from the file extension.<br>
654     *        If destination file does not have a valid extension (for folder for instance) then you
655     *        have to specify a valid Writer to write the image file (see {@link #getWriter(ImageFileFormat)})
656     * @param sequence
657     *        sequence to save
658     * @param file
659     *        file where we want to save sequence.<br>
660     *        Depending the <code>formatWriter</code> the file extension may be modified.<br>
661     *        That is preferred as saving an image with a wrong extension may result in error on
662     *        future read (wrong reader detection).<br>
663     * @param fps
664     *        frame rate for AVI sequence save
665     * @param multipleFile
666     *        flag to indicate if images are saved in separate file.<br>
667     *        When multiple file is enabled the <code>file</code> parameter is considerer as a folder if it doens't have
668     *        any extension
669     * @param showProgress
670     *        show progress bar
671     * @param addToRecent
672     *        add the saved sequence to recent opened sequence list
673     */
674    public static void save(IFormatWriter formatWriter, Sequence sequence, File file, int fps, boolean multipleFile,
675            boolean showProgress, boolean addToRecent)
676    {
677        final String filePath = FileUtil.cleanPath(FileUtil.getGenericPath(file.getAbsolutePath()));
678        final int sizeT = sequence.getSizeT();
679        final int sizeZ = sequence.getSizeZ();
680        final int numImages = sizeT * sizeZ;
681        final FileFrame saveFrame;
682        final ApplicationMenu mainMenu;
683
684        if (addToRecent)
685            mainMenu = Icy.getMainInterface().getApplicationMenu();
686        else
687            mainMenu = null;
688        if (showProgress && !Icy.getMainInterface().isHeadLess())
689            saveFrame = new FileFrame("Saving", filePath);
690        else
691            saveFrame = null;
692        try
693        {
694            if (saveFrame != null)
695            {
696                saveFrame.setLength(numImages);
697                saveFrame.setPosition(0);
698            }
699
700            final IFormatWriter writer;
701            final Sequence savedSequence;
702
703            // get the writer
704            if (formatWriter == null)
705                writer = getWriter(file, ImageFileFormat.TIFF);
706            else
707                writer = formatWriter;
708
709            if (writer == null)
710                throw new UnknownFormatException("Can't find a valid image writer for the specified file: " + file);
711
712            // need multiple files ?
713            if ((numImages > 1) && multipleFile)
714            {
715                // save as severals images
716                final DecimalFormat decimalFormat = new DecimalFormat("0000");
717                final String fileName = FileUtil.getFileName(filePath, false);
718                String fileExt = FileUtil.getFileExtension(filePath, true);
719
720                String fileBaseDirectory = FileUtil.getDirectory(filePath);
721                if (fileBaseDirectory.endsWith("/"))
722                    fileBaseDirectory = fileBaseDirectory.substring(0, fileBaseDirectory.length() - 1);
723
724                // no extension (directory) ?
725                if (StringUtil.isEmpty(fileExt))
726                {
727                    // filename is part of directory
728                    fileBaseDirectory += FileUtil.separator + fileName;
729                    // use the default file extension for the specified writer
730                    fileExt = "." + getImageFileFormat(writer, ImageFileFormat.TIFF).getExtensions()[0];
731                }
732
733                final String filePathWithoutExt = fileBaseDirectory + FileUtil.separator + fileName;
734
735                // create output directory
736                FileUtil.createDir(fileBaseDirectory);
737
738                // default name used --> use filename
739                if (sequence.isDefaultName())
740                    sequence.setName(fileName);
741                sequence.setFilename(fileBaseDirectory);
742                // reset origin informations as now we are saved
743                sequence.resetOriginInformation();
744                // reset image provider
745                sequence.setImageProvider(null);
746
747                // assume that is the saved sequence (used for metadata)
748                savedSequence = sequence;
749
750                for (int t = 0; t < sizeT; t++)
751                {
752                    for (int z = 0; z < sizeZ; z++)
753                    {
754                        String filename = filePathWithoutExt;
755
756                        if (sizeT > 1)
757                            filename += "_t" + decimalFormat.format(t);
758                        if (sizeZ > 1)
759                            filename += "_z" + decimalFormat.format(z);
760                        filename += fileExt;
761
762                        // save as single image file
763                        save(writer, sequence, filename, t, z, fps, saveFrame);
764                    }
765                }
766
767                // add as one item to recent file list
768                if (mainMenu != null)
769                    mainMenu.addRecentFile(fileBaseDirectory);
770            }
771            else
772            {
773                final ImageFileFormat iff = getImageFileFormat(writer, ImageFileFormat.TIFF);
774                final String fileExt = FileUtil.getFileExtension(filePath, false);
775                // force to set correct file extension
776                final String fixedFilePath;
777
778                if (iff.matches(fileExt))
779                    fixedFilePath = filePath;
780                else
781                    fixedFilePath = filePath + "." + iff.getExtensions()[0];
782
783                // default name used --> use filename
784                if (sequence.isDefaultName())
785                    sequence.setName(FileUtil.getFileName(filePath, false));
786
787                // save whole sequence into a single file
788                savedSequence = save(writer, sequence, fixedFilePath, -1, -1, fps, saveFrame);
789
790                // we set filename on actual saved Sequence
791                savedSequence.setFilename(filePath);
792                // reset origin informations as now we are saved
793                sequence.resetOriginInformation();
794
795                // add as one item to recent file list
796                if (mainMenu != null)
797                    mainMenu.addRecentFile(fixedFilePath);
798            }
799
800            // Sequence persistence enabled --> save XML
801            if (GeneralPreferences.getSequencePersistence())
802                savedSequence.saveXMLData();
803        }
804        catch (Exception e)
805        {
806            IcyExceptionHandler.showErrorMessage(e, true);
807            if (showProgress && !Icy.getMainInterface().isHeadLess())
808                new FailedAnnounceFrame("Failed to save image(s) (see output console for details)", 15);
809            return;
810        }
811        finally
812        {
813            if (saveFrame != null)
814                saveFrame.close();
815        }
816    }
817
818    /**
819     * Save a single image from bytes buffer to the specified file.
820     */
821    private static void saveImage(IFormatWriter formatWriter, byte[] data, int width, int height, int numChannel,
822            boolean separateChannel, DataType dataType, File file, boolean force) throws FormatException, IOException
823    {
824        final String filePath = FileUtil.cleanPath(FileUtil.getGenericPath(file.getAbsolutePath()));
825
826        if (FileUtil.exists(filePath))
827        {
828            // forced ? first delete the file else LOCI won't save it
829            if (force)
830                FileUtil.delete(filePath, true);
831            else
832                throw new IOException("File already exists");
833        }
834        // ensure parent directory exist
835        FileUtil.ensureParentDirExist(filePath);
836
837        final IFormatWriter writer;
838        final boolean separateCh;
839
840        if (formatWriter == null)
841        {
842            // get the writer
843            writer = getWriter(FileUtil.getFileExtension(filePath, false), ImageFileFormat.TIFF);
844
845            // prepare the metadata
846            try
847            {
848                separateCh = getSeparateChannelFlag(writer, numChannel, dataType);
849                writer.setMetadataRetrieve((MetadataRetrieve) MetaDataUtil.generateMetaData(width, height, numChannel,
850                        dataType, separateCh));
851            }
852            catch (ServiceException e)
853            {
854                System.err.println("Saver.saveImage(...) error :");
855                IcyExceptionHandler.showErrorMessage(e, true);
856            }
857        }
858        else
859        {
860            // ready to use writer (metadata already prepared)
861            writer = formatWriter;
862            separateCh = separateChannel;
863        }
864
865        // we never interleaved data even if some image viewer need it to correctly read image (win XP viewer)
866        writer.setInterleaved(false);
867        writer.setId(filePath);
868        writer.setSeries(0);
869        // usually give better save performance
870        writer.setWriteSequentially(true);
871
872        try
873        {
874            // separated channel data
875            if (separateChannel)
876            {
877                final int pitch = width * height * dataType.getSize();
878                final byte[] dataChannel = new byte[pitch];
879                int offset = 0;
880
881                for (int c = 0; c < numChannel; c++)
882                {
883                    System.arraycopy(data, offset, dataChannel, 0, pitch);
884                    writer.saveBytes(c, dataChannel);
885                    offset += pitch;
886                }
887            }
888            else
889                // save all data at once
890                writer.saveBytes(0, data);
891        }
892        catch (Exception e)
893        {
894            System.err.println("Saver.saveImage(...) error :");
895            IcyExceptionHandler.showErrorMessage(e, true);
896        }
897
898        writer.close();
899    }
900
901    /**
902     * Save a single image from bytes buffer to the specified file.
903     */
904    public static void saveImage(byte[] data, int width, int height, int numChannel, DataType dataType, File file,
905            boolean force) throws FormatException, IOException
906    {
907        saveImage(null, data, width, height, numChannel, false, dataType, file, force);
908    }
909
910    /**
911     * @deprecated Use {@link #saveImage(byte[], int, int, int, DataType, File, boolean)} instead
912     */
913    @Deprecated
914    public static void saveImage(byte[] data, int width, int height, int numChannel, int dataType,
915            boolean signedDataType, File file, boolean force) throws FormatException, IOException
916    {
917        saveImage(data, width, height, numChannel, DataType.getDataType(dataType, signedDataType), file, force);
918    }
919
920    /**
921     * Save a single image to the specified file
922     * 
923     * @param image
924     * @throws IOException
925     * @throws FormatException
926     */
927    public static void saveImage(IcyBufferedImage image, File file, boolean force) throws FormatException, IOException
928    {
929        final IFormatWriter writer = getWriter(file, ImageFileFormat.TIFF);
930
931        if (writer == null)
932            throw new UnknownFormatException("Can't find a valid image writer for the specified file: " + file);
933
934        final boolean separateChannel = getSeparateChannelFlag(writer, image.getIcyColorModel());
935
936        try
937        {
938            writer.setMetadataRetrieve((MetadataRetrieve) MetaDataUtil.generateMetaData(image, separateChannel));
939        }
940        catch (ServiceException e)
941        {
942            System.err.println("Saver.saveImage(...) error :");
943            IcyExceptionHandler.showErrorMessage(e, true);
944        }
945
946        // get byte order
947        final boolean littleEndian = !writer.getMetadataRetrieve().getPixelsBinDataBigEndian(0, 0).booleanValue();
948        // then save the image
949        saveImage(writer, image.getRawData(littleEndian), image.getSizeX(), image.getSizeY(), image.getSizeC(),
950                separateChannel, image.getDataType_(), file, force);
951    }
952
953    /**
954     * Save the specified sequence in the specified file using the given writer.<br>
955     * If posT or/and posZ are defined then only a sub part of the original Sequence is saved.
956     * 
957     * @param writer
958     *        writer used to save sequence (define the image format, cannot be <code>null</code> at this point)
959     * @param sequence
960     *        sequence to save
961     * @param filePath
962     *        file name where we want to save sequence
963     * @param posT
964     *        frame index to save (-1 to save all frame from input sequence)
965     * @param posZ
966     *        slice index to save (-1 to save all slice from input sequence)
967     * @param fps
968     *        frame rate for AVI writer
969     * @param saveFrame
970     *        progress frame for save operation (can be null)
971     * @return Actual saved Sequence (can be different from input one if conversion was needed)
972     * @throws ServiceException
973     * @throws IOException
974     * @throws FormatException
975     */
976    private static Sequence save(IFormatWriter writer, Sequence sequence, String filePath, int posT, int posZ, int fps,
977            FileFrame saveFrame) throws ServiceException, FormatException, IOException
978    {
979        // TODO: temporary fix for the "incorrect close operation" bug in Bio-Formats
980        // with OME TIF writer, remove it when fixed.
981        // {
982        // try
983        // {
984        // writer = formatWriter.getClass().newInstance();
985        // }
986        // catch (Exception e)
987        // {
988        // throw new ServiceException("Can't create new writer instance: " + e);
989        // }
990        // }
991
992        final File file = new File(filePath);
993
994        // first delete the file else LOCI won't save it correctly
995        if (file.exists())
996            file.delete();
997        // ensure parent directory exist
998        FileUtil.ensureParentDirExist(file);
999
1000        final ImageFileFormat saveFormat = getImageFileFormat(writer, ImageFileFormat.TIFF);
1001        final int sizeT = sequence.getSizeT();
1002        final int sizeZ = sequence.getSizeZ();
1003        final int adjZ, adjT;
1004        final int tMin, tMax;
1005        final int zMin, zMax;
1006
1007        // adjust posT and posZ depending the writer support
1008        switch (saveFormat)
1009        {
1010            default:
1011            case TIFF:
1012                // no restriction for TIFF
1013                adjZ = posZ;
1014                adjT = posT;
1015                break;
1016
1017            case AVI:
1018                // AVI: always save single slice
1019                adjZ = (posZ < 0) ? sizeZ / 2 : posZ;
1020                adjT = posT;
1021                break;
1022
1023            case JPG:
1024            case PNG:
1025                // JPG or PNG: always save single image
1026                adjZ = (posZ < 0) ? sizeZ / 2 : posZ;
1027                adjT = (posT < 0) ? sizeT / 2 : posT;
1028                break;
1029        }
1030
1031        // convert Sequence in good format for specified writer
1032        final Sequence compatibleSequence = getCompatibleSequenceForWriter(writer, sequence, adjT, adjZ);
1033        // get channel separation flag
1034        final boolean separateChannel = getSeparateChannelFlag(saveFormat, compatibleSequence.getColorModel());
1035        // prepare metadata
1036        final OMEXMLMetadata metadata = MetaDataUtil.generateMetaData(compatibleSequence, separateChannel);
1037
1038        // clean unwanted planes
1039        MetaDataUtil.keepPlanes(metadata, 0, adjT, adjZ, -1);
1040        if (adjT < 0)
1041        {
1042            // all frame
1043            tMin = 0;
1044            tMax = sizeT - 1;
1045        }
1046        else
1047        {
1048            // single frame
1049            tMin = tMax = adjT;
1050            MetaDataUtil.setSizeT(metadata, 0, 1);
1051        }
1052        if (adjZ < 0)
1053        {
1054            // all slice
1055            zMin = 0;
1056            zMax = sizeZ - 1;
1057        }
1058        else
1059        {
1060            // single slice
1061            zMin = zMax = adjZ;
1062            MetaDataUtil.setSizeZ(metadata, 0, 1);
1063        }
1064
1065        // specific to TIFF writer
1066        if (writer instanceof TiffWriter)
1067        {
1068            // > 2GB --> use big tiff (important to do it before setId(..) call)
1069            if (MetaDataUtil.getDataSize(metadata, 0, 0) > 2000000000L)
1070                ((TiffWriter) writer).setBigTiff(true);
1071        }
1072
1073        // set settings
1074        writer.setFramesPerSecond(fps);
1075        // generate metadata
1076        writer.setMetadataRetrieve((MetadataRetrieve) metadata);
1077        // no interleave (XP default viewer want interleaved channel to correctly read image)
1078        writer.setInterleaved(false);
1079        // set id
1080        writer.setId(filePath);
1081        // init
1082        writer.setSeries(0);
1083        // usually give better save performance
1084        writer.setWriteSequentially(true);
1085
1086        final int sizeC = compatibleSequence.getSizeC();
1087        // get endianess
1088        final boolean littleEndian = !writer.getMetadataRetrieve().getPixelsBinDataBigEndian(0, 0).booleanValue();
1089        byte[] data = null;
1090
1091        try
1092        {
1093            int imageIndex = 0;
1094            // XYCZT order is important here (see metadata)
1095            for (int t = tMin; t <= tMax; t++)
1096            {
1097                for (int z = zMin; z <= zMax; z++)
1098                {
1099                    // interrupt process (partial save)
1100                    if ((saveFrame != null) && saveFrame.isCancelRequested())
1101                        return compatibleSequence;
1102
1103                    final IcyBufferedImage image = compatibleSequence.getImage(t, z);
1104
1105                    // separated channel data
1106                    if (separateChannel)
1107                    {
1108                        for (int c = 0; c < sizeC; c++)
1109                        {
1110                            if (image != null)
1111                            {
1112                                // avoid multiple allocation
1113                                data = image.getRawData(c, data, 0, littleEndian);
1114                                writer.saveBytes(imageIndex, data);
1115                            }
1116
1117                            imageIndex++;
1118                        }
1119                    }
1120                    else
1121                    {
1122                        if (image != null)
1123                        {
1124                            // avoid multiple allocation
1125                            data = image.getRawData(data, 0, littleEndian);
1126                            writer.saveBytes(imageIndex, data);
1127                        }
1128
1129                        imageIndex++;
1130                    }
1131
1132                    if (saveFrame != null)
1133                        saveFrame.incPosition();
1134                }
1135            }
1136        }
1137        finally
1138        {
1139            // always close writer after a file has been saved
1140            writer.close();
1141        }
1142
1143        return compatibleSequence;
1144    }
1145
1146    /**
1147     * Returns a compatible Sequence representing the input sequence so it can be saved with the specified writer.<br>
1148     * If the writer support the input sequence then the input sequence is directly returned.
1149     * 
1150     * @param writer
1151     *        writer used to save sequence (define the image format, cannot be <code>null</code>)
1152     * @param sequence
1153     *        sequence to save
1154     * @param posT
1155     *        frame index to keep (-1 for all frame)
1156     * @param posZ
1157     *        slice index to keep (-1 for all slice)
1158     * @return the compatible sequence for given Writer
1159     */
1160    public static Sequence getCompatibleSequenceForWriter(IFormatWriter writer, Sequence sequence, int posT, int posZ)
1161    {
1162        final int sizeC = sequence.getSizeC();
1163        final DataType dataType = sequence.getDataType_();
1164        final boolean needConvert;
1165        final ImageFileFormat imageFormat = getImageFileFormat(writer, ImageFileFormat.TIFF);
1166
1167        // adjust posT and posZ depending the writer support
1168        switch (imageFormat)
1169        {
1170            default:
1171                // assume TIFF
1172                needConvert = false;
1173                break;
1174
1175            case AVI:
1176            case JPG:
1177                // JPG, AVI: only supports byte data type and Gray/RGB images
1178                needConvert = (dataType.getSize() > 1) || (sizeC == 2) || (sizeC > 3);
1179                break;
1180
1181            case PNG:
1182                // PNG: support byte data type with a maximum of 4 channels
1183                needConvert = (dataType.getSize() > 1) || (sizeC > 4);
1184                break;
1185        }
1186
1187        // no conversion needed
1188        if (!needConvert)
1189            return sequence;
1190
1191        final int sizeT = sequence.getSizeT();
1192        final int sizeZ = sequence.getSizeZ();
1193        final int tMin, tMax;
1194        final int zMin, zMax;
1195
1196        if (posT < 0)
1197        {
1198            // all frame
1199            tMin = 0;
1200            tMax = sizeT - 1;
1201        }
1202        else
1203            // single frame
1204            tMin = tMax = posT;
1205        if (posZ < 0)
1206        {
1207            // all slice
1208            zMin = 0;
1209            zMax = sizeZ - 1;
1210        }
1211        else
1212            // single slice
1213            zMin = zMax = posZ;
1214
1215        // wanted image type
1216        final int imageType = (sizeC > 1) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_BYTE_GRAY;
1217        // image receiver
1218        final BufferedImage imgOut = new BufferedImage(sequence.getSizeX(), sequence.getSizeY(), imageType);
1219        // conversion LUT (use default sequence one)
1220        final LUT lut = sequence.getDefaultLUT();
1221
1222        // create compatible sequence
1223        final Sequence result = new Sequence(OMEUtil.createOMEXMLMetadata(sequence.getOMEXMLMetadata()));
1224
1225        result.beginUpdate();
1226        try
1227        {
1228            for (int t = tMin; t <= tMax; t++)
1229                for (int z = zMin; z <= zMax; z++)
1230                    result.setImage(t, z, IcyBufferedImageUtil.toBufferedImage(sequence.getImage(t, z), imgOut, lut));
1231
1232            // preserve ROI and overlays (for XML metadata preservation)
1233            for (ROI roi : sequence.getROIs())
1234                result.addROI(roi);
1235            for (Overlay overlay : sequence.getOverlays())
1236                result.addOverlay(overlay);
1237
1238            // rename channels and set final name
1239            switch (imageType)
1240            {
1241                default:
1242                case BufferedImage.TYPE_INT_RGB:
1243                    result.setChannelName(0, "red");
1244                    result.setChannelName(1, "green");
1245                    result.setChannelName(2, "blue");
1246                    break;
1247
1248                case BufferedImage.TYPE_BYTE_GRAY:
1249                    result.setChannelName(0, "gray");
1250                    break;
1251            }
1252            result.setName(sequence.getName() + " (" + imageFormat + ")");
1253        }
1254        finally
1255        {
1256            result.endUpdate();
1257        }
1258
1259        return result;
1260    }
1261}