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.
23 package ini
.trakem2
.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
;
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
) {
59 Utils
.log("Null path, can't save.");
62 final File fdir
= new File(path
).getParentFile();
65 synchronized (OBDIRS
) {
68 } catch (Exception e
) {
69 IJError
.print(e
, true);
70 Utils
.log("Can't use path: " + path
+ "\nCheck your file read/write permissions.");
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
) {
83 Utils
.log("Null ip, can't saveAsJpeg");
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());
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);
105 boolean b
= saveAsJpeg(bi
, path
, quality
, as_grey
);
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;
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
);
124 } catch (Exception e
) {
126 try { f
.close(); } catch (Exception ee
) {}
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
);
142 * java.lang.IllegalArgumentException: NumComponents not in sync with COLOR_ID
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;
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
) {
166 } catch (Exception e
) {
167 // the file might have been generated while trying to read it. So try once more
169 Utils
.log2("JPEG Decoder failed for " + path
);
172 if (null != stream
) { try { stream
.close(); } catch (Exception ee
) {} }
173 stream
= openStream(path
);
175 if (null != stream
) bi
= openJpeg2(stream
, color_id
);
176 } catch (Exception e2
) {
177 IJError
.print(e2
, true);
180 if (null != stream
) { try { stream
.close(); } catch (Exception e
) {} }
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.
195 return new FileInputStream(path
);
196 } catch (FileNotFoundException fnfe
) {
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);
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
) {
220 Utils
.log("Null imp, can't saveAsZip");
223 if (!checkPath(path
)) return false;
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();
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
);
243 catch (IOException e
) {
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();
259 sb
.append("channels="+channels
+"\n");
260 int slices
= imp
.getNSlices();
262 sb
.append("slices="+slices
+"\n");
263 int frames
= imp
.getNFrames();
265 sb
.append("frames="+frames
+"\n");
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
280 if (fi
.pixelDepth
!=0.0 && fi
.pixelDepth
!=1.0)
281 sb
.append("spacing="+fi
.pixelDepth
+"\n");
283 if ((int)cal
.fps
==cal
.fps
)
284 sb
.append("fps="+(int)cal
.fps
+"\n");
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");
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");
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;
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
));
341 } catch (FileNotFoundException fnfe
) {
342 Utils
.log2("saveAsJpegAlpha: Path not found: " + path
);
343 } catch (Exception e
) {
344 IJError
.print(e
, true);
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
;
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
) {
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 );
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);
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);
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
);
390 String path
= "/home/albert/temp/baboonramp.jpg";
391 saveAsJpegAlpha(bi
, path
, 0.75f
);
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);
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
);
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