/**
TrakEM2 plugin for ImageJ(C).
Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
This program 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 (http://www.gnu.org/licenses/gpl.txt )
This program 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 this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
You may contact Albert Cardona at acardona at ini.phys.ethz.ch
Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
**/
package ini.trakem2.io;
import com.sun.media.jai.codec.ImageCodec;
import com.sun.media.jai.codec.TIFFDecodeParam;
import com.sun.media.jai.codec.TIFFEncodeParam;
import ij.ImageJ;
import ij.ImagePlus;
import ij.io.FileInfo;
import ij.io.TiffEncoder;
import ij.measure.Calibration;
import ij.process.ImageProcessor;
import ini.trakem2.persistence.FSLoader;
import ini.trakem2.persistence.Loader;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.media.jai.PlanarImage;
/** Provides the necessary thread-safe image file saver utilities. */
public class ImageSaver {
private ImageSaver() {}
static private final Object OBDIRS = new Object();
/** Will create parent directories if they don't exist.
* <p>
* Returns false if the path is unusable.
* </p>
*/
static public final boolean checkPath(final String path) {
if (null == path) {
Utils.log("Null path, can't save.");
return false;
}
final File fdir = new File(path).getParentFile();
if (!fdir.exists()) {
try {
synchronized (OBDIRS) {
if (!fdir.exists()) { // need to check again, this time inside the synch block
fdir.mkdirs(); // returns false if already exists.
}
return fdir.exists(); // this is what we care about.
// The OS could have created the dirs outside the synch block. So the return value of mkdirs() is insufficient proof.
}
} catch (Exception e) {
IJError.print(e, true);
Utils.log("Can't use path: " + path + "\nCheck your file read/write permissions.");
return false;
}
}
return true;
}
/** *sigh* -- synchronization is needed. Do so by locking each file path independently.
* This is all assuming that file paths will be identical, without cases in which one has a single '/' and another has '//' and so on. */
/** A collection of file paths currently being used for saving images. */
static private final Map<String,Object> pathlocks = new HashMap<String,Object>();
static private final Object getPathLock(final String path) {
Object lock = null;
synchronized (pathlocks) {
lock = pathlocks.get(path);
if (null == lock) {
lock = new Object();
pathlocks.put(path, lock);
}
}
return lock;
}
static private final void removePathLock(final String path) {
synchronized (pathlocks) {
pathlocks.remove(path);
}
}
/** Returns true on success.
* <p>
* Core functionality adapted from {@code ij.plugin.JpegWriter} class by Wayne Rasband.
* </p>
*/
static public final boolean saveAsJpeg(final ImageProcessor ip, final String path, float quality, boolean as_grey) {
// safety checks
if (null == ip) {
Utils.log("Null ip, can't saveAsJpeg");
return false;
}
// ok, onward
// No need to make an RGB int[] image if a byte[] image with a LUT will do.
/*
int image_type = BufferedImage.TYPE_INT_ARGB;
if (ip.getClass().equals(ByteProcessor.class) || ip.getClass().equals(ShortProcessor.class) || ip.getClass().equals(FloatProcessor.class)) {
image_type = BufferedImage.TYPE_BYTE_GRAY;
}
*/
BufferedImage bi = null;
if (as_grey) { // even better would be to make a raster directly from the byte[] array, and pass that to the encoder. Unfortunately, would have to handle specially all non-8-bit images.
bi = new BufferedImage(ip.getWidth(), ip.getHeight(), BufferedImage.TYPE_BYTE_GRAY); //, (IndexColorModel)ip.getColorModel());
} else {
bi = new BufferedImage(ip.getWidth(), ip.getHeight(), BufferedImage.TYPE_INT_RGB);
}
final Graphics g = bi.createGraphics();
final Image awt = ip.createImage();
g.drawImage(awt, 0, 0, null);
g.dispose();
awt.flush();
boolean b = saveAsJpeg(bi, path, quality, as_grey);
bi.flush();
return b;
}
static public final BufferedImage createGrayImage(final byte[] pixels, final int width, final int height) {
WritableRaster wr = Loader.GRAY_LUT.createCompatibleWritableRaster(1, 1);
SampleModel sm = wr.getSampleModel().createCompatibleSampleModel(width, height);
DataBuffer db = new DataBufferByte(pixels, width*height, 0);
WritableRaster raster = Raster.createWritableRaster(sm, db, null);
return new BufferedImage(Loader.GRAY_LUT, raster, false, null);
}
static public final boolean saveAsGreyJpeg(final byte[] pixels, final int width, final int height, final String path, final float quality) {
return saveAsJpeg(createGrayImage(pixels, width, height), path, quality, true);
}
static public final DirectColorModel RGBA_COLOR_MODEL = new DirectColorModel(32, 0xff0000, 0xff00, 0xff, 0xff000000);
static public final DirectColorModel RGBA_PRE_COLOR_MODEL = new DirectColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), 32, 0xff0000, 0xff00, 0xff, 0xff000000, true, DataBuffer.TYPE_INT);
static public final DirectColorModel RGB_COLOR_MODEL = new DirectColorModel(32, 0xff0000, 0xff00, 0xff);
static public final BufferedImage createImage(final int[] pixels, final int width, final int height, final DirectColorModel cm) {
WritableRaster wr = cm.createCompatibleWritableRaster(1, 1);
SampleModel sm = wr.getSampleModel().createCompatibleSampleModel(width, height);
DataBuffer dataBuffer = new DataBufferInt(pixels, width*height, 0);
WritableRaster rgbRaster = Raster.createWritableRaster(sm, dataBuffer, null);
return new BufferedImage(cm, rgbRaster, cm == RGBA_PRE_COLOR_MODEL, null);
}
static public final BufferedImage createRGBImage(final int[] pixels, final int width, final int height) {
return createImage(pixels, width, height, RGB_COLOR_MODEL);
}
static public final BufferedImage createARGBImage(final int[] pixels, final int width, final int height) {
return createImage(pixels, width, height, RGBA_COLOR_MODEL);
}
/** Assumes the pixels have been premultiplied. */
static public final BufferedImage createARGBImagePre(final int[] pixels, final int width, final int height) {
return createImage(pixels, width, height, RGBA_PRE_COLOR_MODEL);
}
/** Save as ARGB jpeg. */
static public final boolean saveAsARGBJpeg(final int[] pixels, final int width, final int height, final String path, final float quality) {
return saveAsJpeg(createARGBImage(pixels, width, height), path, quality, false);
}
/** Will not flush the given BufferedImage. */
static public final boolean saveAsJpeg(final BufferedImage bi, final String path, float quality, boolean as_grey) {
if (!checkPath(path)) return false;
if (quality < 0f) quality = 0f;
if (quality > 1f) quality = 1f;
synchronized (getPathLock(path)) {
ImageOutputStream ios = null;
ImageWriter writer = null;
BufferedImage grey = bi;
try {
writer = ImageIO.getImageWritersByFormatName("jpeg").next();
final ByteArrayOutputStream baos = new ByteArrayOutputStream( estimateJPEGFileSize(bi.getWidth(), bi.getHeight()) );
ios = ImageIO.createImageOutputStream(baos);
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);
if (as_grey && bi.getType() != BufferedImage.TYPE_BYTE_GRAY) {
grey = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
// convert the original colored image to grayscale
// Very slow:
//ColorConvertOp op = new ColorConvertOp(bi.getColorModel().getColorSpace(), grey.getColorModel().getColorSpace(), null);
//op.filter(bi, grey);
// 9 times faster:
grey.createGraphics().drawImage(bi, 0, 0, null);
}
IIOImage iioImage = new IIOImage(grey, null, null);
writer.write(null, iioImage, param);
// Now write to disk
FileChannel ch = null;
try {
// Now write to disk in the fastest way possible
final RandomAccessFile ra = new RandomAccessFile(new File(path), "rw");
final ByteBuffer bb = ByteBuffer.wrap((byte[])Bbuf.get(baos), 0, baos.size());
ch = ra.getChannel();
while (bb.hasRemaining()) {
ch.write(bb);
}
} finally {
if (null != ch) ch.close();
ios.close();
}
} catch (Exception e) {
IJError.print(e);
return false;
} finally {
if (null != writer) try { writer.dispose(); } catch (Exception ee) {}
//if (null != ios) try { ios.close(); } catch (Exception ee) {} // NOT NEEDED, it's a ByteArrayOutputStream
if (bi != grey) try { grey.flush(); /*release native resources*/ } catch (Exception ee) {}
removePathLock(path);
}
}
return true;
}
// Convoluted method to make sure all possibilities of opening and closing the stream are considered.
static public final BufferedImage open(final String path, final boolean as_grey) {
InputStream stream = null;
BufferedImage bi = null;
synchronized (getPathLock(path)) {
try {
// 1 - create a stream if possible
stream = openStream(path);
if (null == stream) return null;
// 2 - open it as a BufferedImage
bi = openFromStream(stream, as_grey);
} catch (Throwable e) {
// the file might have been generated while trying to read it. So try once more
try {
Utils.log2("Decoder failed for " + path);
Thread.sleep(50);
// reopen stream
if (null != stream) { try { stream.close(); } catch (Exception ee) {} }
stream = openStream(path);
// decode
if (null != stream) bi = openFromStream(stream, as_grey);
} catch (Exception e2) {
IJError.print(e2, true);
}
} finally {
removePathLock(path);
if (null != stream) { try { stream.close(); } catch (Exception e) {} }
}
}
return bi;
}
static private final InputStream openStream(final String path) {
/*
// Proper implementation, incurs in big drag because of new File(path).exists() OS calls.
if (FSLoader.isURL(path)) {
return new URL(path).openStream();
} else if (new File(path).exists()) {
return new FileInputStream(path);
}*/
// Simple optimization, incurring in horrible practices ... blame me.
try {
final File f = new File(path);
return new BufferedInputStream(new FileInputStream(f), (int)Math.min(f.length(), 35000000)); // 35 Mb
} catch (FileNotFoundException fnfe) {
try {
if (FSLoader.isURL(path)) {
return new URL(path).openStream();
}
} catch (Throwable e) {
IJError.print(e, true);
}
} catch (Throwable t) {
IJError.print(t, true);
}
return null;
}
/** The stream IS NOT closed by this method. */
static public final BufferedImage openFromStream(final InputStream stream, final boolean as_grey) {
try {
if (as_grey) {
final BufferedImage bi = ImageIO.read(stream);
if (bi.getType() != BufferedImage.TYPE_BYTE_GRAY) {
final BufferedImage grey = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
grey.createGraphics().drawImage(bi, 0, 0, null);
bi.flush();
return grey;
}
return bi;
} else {
return ImageIO.read(stream);
}
} catch (IllegalArgumentException iae) {
// According to the documentation, only occurs
// when stream is null, so this should never happen.
return null;
} catch (IOException ioe) {
return null;
} catch (Throwable t) {
t.printStackTrace();
return null;
}
}
/** Returns true on success.
* <p>
* Core functionality adapted from {@code ij.io.FileSaver} class by Wayne Rasband.
* </p>
*/
static public final boolean saveAsZip(final ImagePlus imp, String path) {
// safety checks
if (null == imp) {
Utils.log("Null imp, can't saveAsZip");
return false;
}
if (!checkPath(path)) return false;
// ok, onward:
FileInfo fi = imp.getFileInfo();
if (!path.endsWith(".zip")) path = path+".zip";
String name = imp.getTitle();
if (name.endsWith(".zip")) name = name.substring(0,name.length()-4);
if (!name.endsWith(".tif")) name = name+".tif";
fi.description = ImageSaver.getDescriptionString(imp, fi);
Object info = imp.getProperty("Info");
if (info!=null && (info instanceof String))
fi.info = (String)info;
fi.sliceLabels = imp.getStack().getSliceLabels();
try {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(path));
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(zos));
zos.putNextEntry(new ZipEntry(name));
TiffEncoder te = new TiffEncoder(fi);
te.write(out);
out.close();
}
catch (IOException e) {
IJError.print(e);
return false;
}
return true;
}
/** Returns a string containing information about the specified image. */
static public final String getDescriptionString(final ImagePlus imp, final FileInfo fi) {
final Calibration cal = imp.getCalibration();
final StringBuilder sb = new StringBuilder(100);
sb.append("ImageJ="+ImageJ.VERSION+"\n");
if (fi.nImages>1 && fi.fileType!=FileInfo.RGB48)
sb.append("images="+fi.nImages+"\n");
int channels = imp.getNChannels();
if (channels>1)
sb.append("channels="+channels+"\n");
int slices = imp.getNSlices();
if (slices>1)
sb.append("slices="+slices+"\n");
int frames = imp.getNFrames();
if (frames>1)
sb.append("frames="+frames+"\n");
if (fi.unit!=null)
sb.append("unit="+fi.unit+"\n");
if (fi.valueUnit!=null && fi.calibrationFunction!=Calibration.CUSTOM) {
sb.append("cf="+fi.calibrationFunction+"\n");
if (fi.coefficients!=null) {
for (int i=0; i<fi.coefficients.length; i++)
sb.append("c"+i+"="+fi.coefficients[i]+"\n");
}
sb.append("vunit="+fi.valueUnit+"\n");
if (cal.zeroClip()) sb.append("zeroclip=true\n");
}
// get stack z-spacing and fps
if (fi.nImages>1) {
if (fi.pixelDepth!=0.0 && fi.pixelDepth!=1.0)
sb.append("spacing="+fi.pixelDepth+"\n");
if (cal.fps!=0.0) {
if ((int)cal.fps==cal.fps)
sb.append("fps="+(int)cal.fps+"\n");
else
sb.append("fps="+cal.fps+"\n");
}
sb.append("loop="+(cal.loop?"true":"false")+"\n");
if (cal.frameInterval!=0.0) {
if ((int)cal.frameInterval==cal.frameInterval)
sb.append("finterval="+(int)cal.frameInterval+"\n");
else
sb.append("finterval="+cal.frameInterval+"\n");
}
if (!cal.getTimeUnit().equals("sec"))
sb.append("tunit="+cal.getTimeUnit()+"\n");
}
// get min and max display values
final ImageProcessor ip = imp.getProcessor();
final double min = ip.getMin();
final double max = ip.getMax();
final int type = imp.getType();
final boolean enhancedLut = (type==ImagePlus.GRAY8 || type==ImagePlus.COLOR_256) && (min!=0.0 || max !=255.0);
if (enhancedLut || type==ImagePlus.GRAY16 || type==ImagePlus.GRAY32) {
sb.append("min="+min+"\n");
sb.append("max="+max+"\n");
}
// get non-zero origins
if (cal.xOrigin!=0.0)
sb.append("xorigin="+cal.xOrigin+"\n");
if (cal.yOrigin!=0.0)
sb.append("yorigin="+cal.yOrigin+"\n");
if (cal.zOrigin!=0.0)
sb.append("zorigin="+cal.zOrigin+"\n");
if (cal.info!=null && cal.info.length()<=64 && cal.info.indexOf('=')==-1 && cal.info.indexOf('\n')==-1)
sb.append("info="+cal.info+"\n");
sb.append((char)0);
return new String(sb);
}
static public final Field Bbuf;
static {
Field b = null;
try {
b = ByteArrayOutputStream.class.getDeclaredField("buf");
b.setAccessible(true);
} catch (Exception e) {
IJError.print(e);
}
Bbuf = b;
}
/** Based on EM images of neuropils with outside alpha masks.
* Fitted a polynomial on the length of file vs area size. */
static public final int estimateJPEGFileSize(final int w, final int h) {
final long area = w * h;
return (int)((0.0000000108018 * area * area + 0.315521 * area + 8283.24) * 1.2); // 20% padding
}
/** 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. */
static public final boolean saveAsJpegAlpha(final BufferedImage awt, final String path, final float quality) {
if (!checkPath(path)) return false;
synchronized (getPathLock(path)) {
try {
// 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.
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next(); // just the first one
if (null != writer) {
ImageWriteParam iwp = writer.getDefaultWriteParam(); // with all jpeg specs in it
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionQuality(quality); // <---------------------------------------------------------- THIS IS ALL I WANTED
//((JPEGImageWriteParam)iwp).setProgressiveMode(JPEGImageWriteParam.MODE_DISABLED);
//((JPEGImageWriteParam)iwp).
//
final ByteArrayOutputStream baos = new ByteArrayOutputStream( estimateJPEGFileSize(awt.getWidth(), awt.getHeight()) );
ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
RandomAccessFile ra = null;
FileChannel ch = null;
try {
writer.setOutput(ios);
writer.write(writer.getDefaultStreamMetadata(iwp), new IIOImage(awt, null, null), iwp);
// Now write to disk in the fastest way possible
ra = new RandomAccessFile(new File(path), "rw");
final ByteBuffer bb = ByteBuffer.wrap((byte[])Bbuf.get(baos), 0, baos.size());
ch = ra.getChannel();
while (bb.hasRemaining()) {
ch.write(bb);
}
ch.force(false);
} finally {
if (null != ch) ch.close();
ios.close();
}
return true; // only one: com.sun.imageio.plugins.jpeg.JPEGImageWriter
}
// If the above doesn't find any, magically do it anyway without setting the compression quality:
ImageIO.write(awt, "jpeg", new File(path));
return true;
} catch (FileNotFoundException fnfe) {
Utils.log2("saveAsJpegAlpha: Path not found: " + path);
} catch (Exception e) {
IJError.print(e, true);
} finally {
removePathLock(path);
}
}
return false;
}
/** 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. */
static public final boolean saveAsJpegAlpha(final Image awt, final String path, final float quality) {
final BufferedImage bi = asBufferedImage(awt);
boolean b = saveAsJpegAlpha(bi, path, quality);
if (bi != awt) bi.flush();
return b;
}
static public final BufferedImage asBufferedImage(final Image awt) {
BufferedImage bi = null;
if (awt instanceof BufferedImage) {
bi = (BufferedImage)awt;
} else {
bi = new BufferedImage(awt.getWidth(null), awt.getHeight(null), BufferedImage.TYPE_INT_ARGB);
bi.createGraphics().drawImage(awt, 0, 0, null);
}
return bi;
}
/** Open a jpeg file including the alpha channel if it has one. */
static public BufferedImage openJpegAlpha(final String path) {
return openImage(path, true);
}
static public BufferedImage openPNGAlpha(final String path) {
return openImage(path, true);
}
/** Open an image file including the alpha channel if it has one; will open JPEG, PNG and BMP.
* @param ensure_premultiplied_alpha when true, ALWAYS puts the loaded image into a TYPE_INT_ARGB_PRE, which ensures for example PNG images with an alpha channel but of TYPE_CUSTOM to be premultiplied as well. */
static public BufferedImage openImage(final String path, final boolean ensure_premultiplied_alpha) {
synchronized (getPathLock(path)) {
try {
final BufferedImage img = ImageIO.read(new File(path));
if (ensure_premultiplied_alpha || img.getType() == BufferedImage.TYPE_INT_ARGB || img.getType() == BufferedImage.TYPE_CUSTOM) {
// Premultiply alpha, for speed (makes a huge difference)
final BufferedImage imgPre = new BufferedImage( img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE );
imgPre.createGraphics().drawImage( img, 0, 0, null );
img.flush();
return imgPre;
}
return img;
} catch (FileNotFoundException fnfe) {
Utils.log2("openImage: Path not found: " + path);
} catch (Exception e) {
Utils.log2("openImage: cannot open " + path);
//IJError.print(e, true);
} finally {
removePathLock(path);
}
}
return null;
}
static public BufferedImage openGreyImage(final String path) {
synchronized (getPathLock(path)) {
try {
return asGrey(ImageIO.read(new File(path)));
} catch (FileNotFoundException fnfe) {
Utils.log2("openImage: Path not found: " + path);
} catch (Exception e) {
Utils.log2("openImage: cannot open " + path);
//IJError.print(e, true);
} finally {
removePathLock(path);
}
}
return null;
}
/** If the given BufferedImage is of type TYPE_BYTE_GRAY, it will simply return it. If not, it will flush() the given BufferedImage, and return a new grey one. */
static public final BufferedImage asGrey(final BufferedImage bi) {
if (null == bi) return null;
if (bi.getType() == BufferedImage.TYPE_BYTE_GRAY) {
return bi;
}
// Else:
final BufferedImage grey = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
grey.createGraphics().drawImage(bi, 0, 0, null);
bi.flush();
return grey;
}
static public final void debugAlpha() {
// create an image with an alpha channel
BufferedImage bi = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB);
// get an image without alpha channel to paste into it
Image baboon = new ij.io.Opener().openImage("http://rsb.info.nih.gov/ij/images/baboon.jpg").getProcessor().createImage();
bi.createGraphics().drawImage(baboon, 0, 0, null);
baboon.flush();
// create a fading alpha channel
int[] ramp = (int[])ij.gui.NewImage.createRGBImage("ramp", 512, 512, 1, ij.gui.NewImage.FILL_RAMP).getProcessor().getPixels();
// insert fading alpha ramp into the image
bi.getAlphaRaster().setPixels(0, 0, 512, 512, ramp);
// save the image
String path = "/home/albert/temp/baboonramp.jpg";
saveAsJpegAlpha(bi, path, 0.75f);
// open the image
Image awt = openJpegAlpha(path);
// show it in a canvas that has some background
// so that if the alpha was read from the jpeg file, it is readily visible
javax.swing.JFrame frame = new javax.swing.JFrame("test alpha");
final Image background = frame.getGraphicsConfiguration().createCompatibleImage(512, 512);
final Image some = new ij.io.Opener().openImage("http://rsb.info.nih.gov/ij/images/bridge.gif").getProcessor().createImage();
java.awt.Graphics g = background.getGraphics();
g.drawImage(some, 0, 0, null);
some.flush();
g.drawImage(awt, 0, 0, null);
@SuppressWarnings("serial")
java.awt.Canvas canvas = new java.awt.Canvas() {
public void paint(Graphics g) {
g.drawImage(background, 0, 0, null);
}
};
canvas.setSize(512, 512);
frame.getContentPane().add(canvas);
frame.pack();
frame.setVisible(true);
// 1) check if 8-bit images can also be jpegs with an alpha channel: they can't
// 2) check if ImagePlus preserves the alpha channel as well: it doesn't
}
static public final boolean saveAsPNG(final ImageProcessor ip, final String path) {
Image awt = null;
try {
awt = ip.createImage();
return ImageSaver.saveAsPNG(awt, path);
} catch (Exception e) {
IJError.print(e);
return false;
} finally {
if (null != awt) awt.flush();
}
}
static public final boolean saveAsPNG(final Image awt, final String path) {
try {
BufferedImage bi = null;
if (awt instanceof BufferedImage) {
bi = (BufferedImage)awt;
} else {
bi = new BufferedImage(awt.getWidth(null), awt.getHeight(null), BufferedImage.TYPE_INT_ARGB);
bi.createGraphics().drawImage(awt, 0, 0, null);
}
return saveAsPNG(bi, path);
} catch (Exception e) {
IJError.print(e);
return false;
}
}
/** Save a PNG with or without alpha channel with default compression at maximum level (9, expressed as 0 in the ImageWriter compression quality because 0 indicates "maximum compression is important").*/
static public final boolean saveAsPNG(final BufferedImage awt, final String path) {
if (!checkPath(path)) return false;
synchronized (getPathLock(path)) {
try {
// java.lang.UnsupportedOperationException: Compression not supported (!)
/*
// This is all the mid-level junk code I have to learn and manage just to SET THE F*CK*NG compression level for a PNG
ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next(); // just the first one
if (null != writer) {
ImageWriteParam iwp = writer.getDefaultWriteParam(); // with all PNG specs in it
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionQuality(0); // <---------------------------------------------------------- THIS IS ALL I WANTED
ImageOutputStream ios = ImageIO.createImageOutputStream(new File(path));
try {
writer.setOutput(ios); // the stream
writer.write(writer.getDefaultStreamMetadata(iwp), new IIOImage(awt, null, null), iwp);
} finally {
ios.close();
}
return true;
}
*/
// If the above doesn't find any, magically do it anyway without setting the compression quality:
return ImageIO.write(awt, "png", new File(path));
} catch (FileNotFoundException fnfe) {
Utils.log2("saveAsPng: Path not found: " + path);
} catch (Exception e) {
IJError.print(e, true);
} finally {
removePathLock(path);
}
}
return false;
}
/** WARNING fails when there is an alpha channel: generates an empty file. */
/*
static public final boolean saveAsBMP(final BufferedImage awt, final String path) {
if (!checkPath(path)) return false;
synchronized (getPathLock(path)) {
try {
return ImageIO.write(awt, "bmp", new File(path));
} catch (FileNotFoundException fnfe) {
Utils.log2("saveAsPng: Path not found: " + path);
} catch (Exception e) {
IJError.print(e, true);
} finally {
removePathLock(path);
}
}
return false;
}
*/
static public final boolean saveAsTIFF(final ImageProcessor ip, final String path, final boolean as_grey) {
try {
return saveAsTIFF(ip.getBufferedImage(), path, as_grey);
} catch (Exception e) {
IJError.print(e);
}
return false;
}
/** Will not flush @param awt. */
static public final boolean saveAsTIFF(final Image awt, final String path, final boolean as_grey) {
BufferedImage bi = null;
try {
if (awt instanceof BufferedImage) return saveAsTIFF((BufferedImage)awt, path, as_grey);
// Else, transform into BufferedImage (which is a RenderedImage):
bi = new BufferedImage(awt.getWidth(null), awt.getHeight(null), as_grey ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_INT_ARGB);
bi.createGraphics().drawImage(awt, 0, 0, null);
return saveAsTIFF(bi, path, false); // no need for more checks for grey
} catch (Exception e) {
IJError.print(e);
} finally {
if (null != bi) bi.flush();
}
return false;
}
static public final boolean saveAsTIFF(BufferedImage bi, final String path, final boolean as_grey) {
if (!checkPath(path)) return false;
synchronized (getPathLock(path)) {
OutputStream out = null;
try {
if (as_grey) bi = asGrey(bi);
final TIFFEncodeParam param = new TIFFEncodeParam();
// If the bi is larger than 512x512, I could use COMPRESSION_LZW or COMPRESSION_DEFLATE (zip-in-tiff, lossless), or COMPRESSION_JPEG_TTN2 (Jpeg-in-tiff) -- i.e. an adaptive strategy as suggested by Clay Reid
param.setCompression(TIFFEncodeParam.COMPRESSION_NONE);
out = new BufferedOutputStream(new FileOutputStream(path));
ImageCodec.createImageEncoder("TIFF", out, param).encode(bi);
out.flush(); // !@#$% Couldn't it do it by itself?
final File f = new File(path);
return f.exists() && f.length() > 0; // no other way to check if the writing was successful
} catch (FileNotFoundException fnfe) {
Utils.log2("saveAsTIFF: Path not found: " + path);
} catch (Exception e) {
IJError.print(e, true);
} finally {
if (null != out) try { out.close(); } catch (Exception e) {}
removePathLock(path);
}
}
return false;
}
// WARNING JAI is fragile, throws an Exception when reading malformed tif files (like tif files whose OutputStream was not flush()'ed)
static private final BufferedImage openTIFF(final String path) throws Exception {
/*
final RenderedImage ri = ImageCodec.createImageDecoder("TIFF", new File(path), new TIFFDecodeParam()).decodeAsRenderedImage();
final PlanarImage pi = PlanarImage.wrapRenderedImage(ri);
final BufferedImage img = pi.getAsBufferedImage();
*/
return PlanarImage.wrapRenderedImage(ImageCodec.createImageDecoder("TIFF", new File(path), new TIFFDecodeParam()).decodeAsRenderedImage()).getAsBufferedImage();
}
/** Opens RGB or RGB + alpha images stored in TIFF format. */
static public final BufferedImage openTIFF(final String path, final boolean ensure_premultiplied_alpha) {
synchronized (getPathLock(path)) {
try {
final BufferedImage img = openTIFF(path);
if (ensure_premultiplied_alpha || img.getType() == BufferedImage.TYPE_INT_ARGB || img.getType() == BufferedImage.TYPE_CUSTOM) {
// Premultiply alpha, for speed (makes a huge difference)
final BufferedImage imgPre = new BufferedImage( img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE );
imgPre.createGraphics().drawImage( img, 0, 0, null );
img.flush();
return imgPre;
}
return img;
} catch (Exception e) {
Utils.log2("openTIFF: cannot open " + path + " :\n\t" + e);
//IJError.print(e, true);
} finally {
removePathLock(path);
}
}
return null;
}
static public final BufferedImage openGreyTIFF(final String path) {
synchronized (getPathLock(path)) {
try {
return asGrey(openTIFF(path));
} catch (Exception e) {
Utils.log2("openGreyTIFF: cannot open " + path + " :\n\t" + e);
//IJError.print(e, true);
} finally {
removePathLock(path);
}
}
return null;
}
}