Updated copyright dates.
[trakem2.git] / ini / trakem2 / io / ImageSaver.java
blob7d4764f9088c2368f2824019d4d1abf13424d694
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.io;
25 import ij.ImagePlus;
26 import ij.ImageJ;
27 import ij.process.*;
28 import ij.io.*;
29 import ij.measure.Calibration;
31 import com.sun.image.codec.jpeg.*;
32 import java.awt.image.*;
33 import java.awt.Graphics;
34 import java.awt.Image;
35 import java.io.*;
36 import java.net.URL;
37 import java.util.zip.*;
38 import javax.imageio.ImageIO;
39 import javax.imageio.ImageWriter;
40 import javax.imageio.ImageWriteParam;
41 import javax.imageio.IIOImage;
43 import ini.trakem2.utils.Utils;
44 import ini.trakem2.utils.IJError;
45 import ini.trakem2.persistence.FSLoader;
47 /** Provides the necessary thread-safe image file saver utilities. */
48 public class ImageSaver {
50 private ImageSaver() {}
52 static private final Object OBDIRS = new Object();
54 /** Will create parent directories if they don't exist.<br />
55 * Returns false if the path is unusable.
57 static private final boolean checkPath(final String path) {
58 if (null == path) {
59 Utils.log("Null path, can't save.");
60 return false;
62 final File fdir = new File(path).getParentFile();
63 if (!fdir.exists()) {
64 try {
65 synchronized (OBDIRS) {
66 return fdir.mkdirs();
68 } catch (Exception e) {
69 IJError.print(e, true);
70 Utils.log("Can't use path: " + path + "\nCheck your file read/write permissions.");
71 return false;
74 return true;
77 /** Returns true on success.<br />
78 * Core functionality adapted from ij.plugin.JpegWriter class by Wayne Rasband.
80 static public final boolean saveAsJpeg(final ImageProcessor ip, final String path, float quality, boolean as_grey) {
81 // safety checks
82 if (null == ip) {
83 Utils.log("Null ip, can't saveAsJpeg");
84 return false;
86 // ok, onward
87 // No need to make an RGB int[] image if a byte[] image with a LUT will do.
89 int image_type = BufferedImage.TYPE_INT_ARGB;
90 if (ip.getClass().equals(ByteProcessor.class) || ip.getClass().equals(ShortProcessor.class) || ip.getClass().equals(FloatProcessor.class)) {
91 image_type = BufferedImage.TYPE_BYTE_GRAY;
94 BufferedImage bi = null;
95 if (as_grey) { // even better would be to make a raster directly from the byte[] array, and pass that to the encoder
96 bi = new BufferedImage(ip.getWidth(), ip.getHeight(), BufferedImage.TYPE_BYTE_GRAY); //, (IndexColorModel)ip.getColorModel());
97 } else {
98 bi = new BufferedImage(ip.getWidth(), ip.getHeight(), BufferedImage.TYPE_INT_RGB);
100 final Graphics g = bi.createGraphics();
101 final Image awt = ip.createImage();
102 g.drawImage(awt, 0, 0, null);
103 g.dispose();
104 awt.flush();
105 boolean b = saveAsJpeg(bi, path, quality, as_grey);
106 bi.flush();
107 return b;
110 /** Will not flush the given BufferedImage. */
111 static public final boolean saveAsJpeg(final BufferedImage bi, final String path, float quality, boolean as_grey) {
112 if (!checkPath(path)) return false;
113 if (quality < 0f) quality = 0f;
114 if (quality > 1f) quality = 1f;
115 FileOutputStream f = null;
116 try {
117 f = new FileOutputStream(path);
118 final JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(f);
119 final JPEGEncodeParam param = as_grey ? encoder.getDefaultJPEGEncodeParam(bi.getRaster(), JPEGDecodeParam.COLOR_ID_GRAY)
120 : encoder.getDefaultJPEGEncodeParam(bi);
121 param.setQuality(quality, true);
122 encoder.encode(bi, param);
123 f.close();
124 } catch (Exception e) {
125 if (null != f) {
126 try { f.close(); } catch (Exception ee) {}
128 IJError.print(e);
129 return false;
131 return true;
134 /** Open a jpeg image that is known to be grayscale.<br />
135 * This method avoids having to open it as int[] (4 times as big!) and then convert it to grayscale by looping through all its pixels and comparing if all three channels are the same (which, least you don't know, is what ImageJ 139j and before does).
137 static public final BufferedImage openGreyJpeg(final String path) {
138 return openJpeg(path, JPEGDecodeParam.COLOR_ID_GRAY);
141 /* // ERROR:
142 * java.lang.IllegalArgumentException: NumComponents not in sync with COLOR_ID
143 * uh?
146 static public final BufferedImage openColorJpeg(final String path) throws Exception {
147 return openJpeg(path, JPEGDecodeParam.COLOR_ID_RGB);
151 // Convoluted method to make sure all possibilities of opening and closing the stream are considered.
152 static private final BufferedImage openJpeg(final String path, final int color_id) {
153 InputStream stream = null;
154 BufferedImage bi = null;
155 try {
157 // 1 - create a stream if possible
158 stream = openStream(path);
159 if (null == stream) return null;
161 // 2 - open it as a BufferedImage
162 bi = openJpeg2(stream, color_id);
164 } catch (FileNotFoundException fnfe) {
165 bi = null;
166 } catch (Exception e) {
167 // the file might have been generated while trying to read it. So try once more
168 try {
169 Utils.log2("JPEG Decoder failed for " + path);
170 Thread.sleep(50);
171 // reopen stream
172 if (null != stream) { try { stream.close(); } catch (Exception ee) {} }
173 stream = openStream(path);
174 // decode
175 if (null != stream) bi = openJpeg2(stream, color_id);
176 } catch (Exception e2) {
177 IJError.print(e2, true);
179 } finally {
180 if (null != stream) { try { stream.close(); } catch (Exception e) {} }
182 return bi;
185 static private final InputStream openStream(final String path) throws Exception {
187 // Proper implementation, incurs in big drag because of new File(path).exists() OS calls.
188 if (FSLoader.isURL(path)) {
189 return new URL(path).openStream();
190 } else if (new File(path).exists()) {
191 return new FileInputStream(path);
193 // Simple optimization, incurring in horrible practices ... blame me.
194 try {
195 return new FileInputStream(path);
196 } catch (FileNotFoundException fnfe) {
197 try {
198 if (FSLoader.isURL(path)) {
199 return new URL(path).openStream();
201 } catch (Throwable e) {
202 IJError.print(e, true);
204 } catch (Throwable t) {
205 IJError.print(t, true);
207 return null;
210 static private final BufferedImage openJpeg2(final InputStream stream, final int color_id) throws Exception {
211 return JPEGCodec.createJPEGDecoder(stream, JPEGCodec.getDefaultJPEGEncodeParam(1, color_id)).decodeAsBufferedImage();
214 /** Returns true on success.<br />
215 * Core functionality adapted from ij.io.FileSaver class by Wayne Rasband.
217 static public final boolean saveAsZip(final ImagePlus imp, String path) {
218 // safety checks
219 if (null == imp) {
220 Utils.log("Null imp, can't saveAsZip");
221 return false;
223 if (!checkPath(path)) return false;
224 // ok, onward:
225 FileInfo fi = imp.getFileInfo();
226 if (!path.endsWith(".zip")) path = path+".zip";
227 String name = imp.getTitle();
228 if (name.endsWith(".zip")) name = name.substring(0,name.length()-4);
229 if (!name.endsWith(".tif")) name = name+".tif";
230 fi.description = ImageSaver.getDescriptionString(imp, fi);
231 Object info = imp.getProperty("Info");
232 if (info!=null && (info instanceof String))
233 fi.info = (String)info;
234 fi.sliceLabels = imp.getStack().getSliceLabels();
235 try {
236 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(path));
237 DataOutputStream out = new DataOutputStream(new BufferedOutputStream(zos));
238 zos.putNextEntry(new ZipEntry(name));
239 TiffEncoder te = new TiffEncoder(fi);
240 te.write(out);
241 out.close();
243 catch (IOException e) {
244 IJError.print(e);
245 return false;
247 return true;
250 /** Returns a string containing information about the specified image. */
251 static public final String getDescriptionString(final ImagePlus imp, final FileInfo fi) {
252 final Calibration cal = imp.getCalibration();
253 final StringBuffer sb = new StringBuffer(100);
254 sb.append("ImageJ="+ImageJ.VERSION+"\n");
255 if (fi.nImages>1 && fi.fileType!=FileInfo.RGB48)
256 sb.append("images="+fi.nImages+"\n");
257 int channels = imp.getNChannels();
258 if (channels>1)
259 sb.append("channels="+channels+"\n");
260 int slices = imp.getNSlices();
261 if (slices>1)
262 sb.append("slices="+slices+"\n");
263 int frames = imp.getNFrames();
264 if (frames>1)
265 sb.append("frames="+frames+"\n");
266 if (fi.unit!=null)
267 sb.append("unit="+fi.unit+"\n");
268 if (fi.valueUnit!=null && fi.calibrationFunction!=Calibration.CUSTOM) {
269 sb.append("cf="+fi.calibrationFunction+"\n");
270 if (fi.coefficients!=null) {
271 for (int i=0; i<fi.coefficients.length; i++)
272 sb.append("c"+i+"="+fi.coefficients[i]+"\n");
274 sb.append("vunit="+fi.valueUnit+"\n");
275 if (cal.zeroClip()) sb.append("zeroclip=true\n");
278 // get stack z-spacing and fps
279 if (fi.nImages>1) {
280 if (fi.pixelDepth!=0.0 && fi.pixelDepth!=1.0)
281 sb.append("spacing="+fi.pixelDepth+"\n");
282 if (cal.fps!=0.0) {
283 if ((int)cal.fps==cal.fps)
284 sb.append("fps="+(int)cal.fps+"\n");
285 else
286 sb.append("fps="+cal.fps+"\n");
288 sb.append("loop="+(cal.loop?"true":"false")+"\n");
289 if (cal.frameInterval!=0.0) {
290 if ((int)cal.frameInterval==cal.frameInterval)
291 sb.append("finterval="+(int)cal.frameInterval+"\n");
292 else
293 sb.append("finterval="+cal.frameInterval+"\n");
295 if (!cal.getTimeUnit().equals("sec"))
296 sb.append("tunit="+cal.getTimeUnit()+"\n");
299 // get min and max display values
300 final ImageProcessor ip = imp.getProcessor();
301 final double min = ip.getMin();
302 final double max = ip.getMax();
303 final int type = imp.getType();
304 final boolean enhancedLut = (type==ImagePlus.GRAY8 || type==ImagePlus.COLOR_256) && (min!=0.0 || max !=255.0);
305 if (enhancedLut || type==ImagePlus.GRAY16 || type==ImagePlus.GRAY32) {
306 sb.append("min="+min+"\n");
307 sb.append("max="+max+"\n");
310 // get non-zero origins
311 if (cal.xOrigin!=0.0)
312 sb.append("xorigin="+cal.xOrigin+"\n");
313 if (cal.yOrigin!=0.0)
314 sb.append("yorigin="+cal.yOrigin+"\n");
315 if (cal.zOrigin!=0.0)
316 sb.append("zorigin="+cal.zOrigin+"\n");
317 if (cal.info!=null && cal.info.length()<=64 && cal.info.indexOf('=')==-1 && cal.info.indexOf('\n')==-1)
318 sb.append("info="+cal.info+"\n");
319 sb.append((char)0);
320 return new String(sb);
323 /** Save an RGB jpeg including the alpha channel if it has one; can be read only by ImageSaver.openJpegAlpha method; in other software the alpha channel is confused by some other color channel. */
324 static public final boolean saveAsJpegAlpha(final BufferedImage awt, final String path, final float quality) {
325 if (!checkPath(path)) return false;
326 try {
327 // This is all the mid-level junk code I have to learn and manage just to SET THE F*CK*NG compression quality for a jpeg.
328 ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next(); // just the first one
329 if (null != writer) {
330 ImageWriteParam iwp = writer.getDefaultWriteParam(); // with all jpeg specs in it
331 iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
332 iwp.setCompressionQuality(quality); // <---------------------------------------------------------- THIS IS ALL I WANTED
333 writer.setOutput(ImageIO.createImageOutputStream(new File(path))); // the stream
334 writer.write(writer.getDefaultStreamMetadata(iwp), new IIOImage(awt, null, null), iwp);
335 return true; // only one: com.sun.imageio.plugins.jpeg.JPEGImageWriter
338 // If the above doesn't find any, magically do it anyway without setting the compression quality:
339 ImageIO.write(awt, "jpeg", new File(path));
340 return true;
341 } catch (FileNotFoundException fnfe) {
342 Utils.log2("saveAsJpegAlpha: Path not found: " + path);
343 } catch (Exception e) {
344 IJError.print(e, true);
346 return false;
349 /** Save an RGB jpeg including the alpha channel if it has one; can be read only by ImageSaver.openJpegAlpha method; in other software the alpha channel is confused by some other color channel. */
350 static public final boolean saveAsJpegAlpha(final Image awt, final String path, final float quality) {
351 BufferedImage bi = null;
352 if (awt instanceof BufferedImage) {
353 bi = (BufferedImage)awt;
354 } else {
355 bi = new BufferedImage(awt.getWidth(null), awt.getHeight(null), BufferedImage.TYPE_INT_ARGB);
356 bi.createGraphics().drawImage(awt, 0, 0, null);
358 return saveAsJpegAlpha(bi, path, quality);
361 /** Open a jpeg file including the alpha channel if it has one. */
362 static public BufferedImage openJpegAlpha(final String path) {
363 try {
364 final BufferedImage img = ImageIO.read(new File(path));
365 BufferedImage imgPre = new BufferedImage( img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE );
366 imgPre.createGraphics().drawImage( img, 0, 0, null );
367 img.flush();
368 return imgPre;
369 } catch (FileNotFoundException fnfe) {
370 Utils.log2("openJpegAlpha: Path not found: " + path);
371 } catch (Exception e) {
372 Utils.log2("openJpegAlpha: cannot open " + path);
373 //IJError.print(e, true);
375 return null;
378 static public final void debugAlpha() {
379 // create an image with an alpha channel
380 BufferedImage bi = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB);
381 // get an image without alpha channel to paste into it
382 Image baboon = new ij.io.Opener().openImage("http://rsb.info.nih.gov/ij/images/baboon.jpg").getProcessor().createImage();
383 bi.createGraphics().drawImage(baboon, 0, 0, null);
384 baboon.flush();
385 // create a fading alpha channel
386 int[] ramp = (int[])ij.gui.NewImage.createRGBImage("ramp", 512, 512, 1, ij.gui.NewImage.FILL_RAMP).getProcessor().getPixels();
387 // insert fading alpha ramp into the image
388 bi.getAlphaRaster().setPixels(0, 0, 512, 512, ramp);
389 // save the image
390 String path = "/home/albert/temp/baboonramp.jpg";
391 saveAsJpegAlpha(bi, path, 0.75f);
392 // open the image
393 Image awt = openJpegAlpha(path);
394 // show it in a canvas that has some background
395 // so that if the alpha was read from the jpeg file, it is readily visible
396 javax.swing.JFrame frame = new javax.swing.JFrame("test alpha");
397 final Image background = frame.getGraphicsConfiguration().createCompatibleImage(512, 512);
398 final Image some = new ij.io.Opener().openImage("http://rsb.info.nih.gov/ij/images/bridge.gif").getProcessor().createImage();
399 java.awt.Graphics g = background.getGraphics();
400 g.drawImage(some, 0, 0, null);
401 some.flush();
402 g.drawImage(awt, 0, 0, null);
403 java.awt.Canvas canvas = new java.awt.Canvas() {
404 public void paint(Graphics g) {
405 g.drawImage(background, 0, 0, null);
408 canvas.setSize(512, 512);
409 frame.getContentPane().add(canvas);
410 frame.pack();
411 frame.setVisible(true);
413 // 1) check if 8-bit images can also be jpegs with an alpha channel: they can't
414 // 2) check if ImagePlus preserves the alpha channel as well: it doesn't