package ij.plugin.filter;
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.io.*;
import ij.plugin.Animator;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.ImageIO;
public class AVI_Writer implements PlugInFilter {
public final static int NO_COMPRESSION = 0; public final static int JPEG_COMPRESSION = 0x47504a4d; public final static int PNG_COMPRESSION = 0x20676e70; private final static int FOURCC_00db = 0x62643030; private final static int FOURCC_00dc = 0x63643030; private final static int MAX_INDX_SIZE = 3072; private final static int JUNK_SIZE_THRESHOLD = 950*1024*1024; private int compressionIndex = 2; private static int jpegQuality = 90; private final static String[] COMPRESSION_STRINGS = new String[] {"None", "PNG", "JPEG"};
private final static int[] COMPRESSION_TYPES = new int[] {NO_COMPRESSION, PNG_COMPRESSION, JPEG_COMPRESSION};
private ImagePlus imp;
private RandomAccessFile raFile;
private int xDim,yDim; private int zDim; private int bytesPerPixel; private int frameDataSize; private int biCompression; private int linePad; private byte[] bufferWrite; private BufferedImage bufferedImage; private RaOutputStream raOutputStream; private long[] sizePointers = new long[5]; private int stackPointer; private int endHeadPointer; private long pointer2indx; private int nIndxEntries=0; private long pointer2indxNEntriesInUse; private long pointer2indxNextEntry;
public int setup(String arg, ImagePlus imp) {
this.imp = imp;
return DOES_ALL+NO_CHANGES;
}
public void run(ImageProcessor ip) {
if (!showDialog(imp)) return; SaveDialog sd = new SaveDialog("Save as AVI...", imp.getTitle(), ".avi");
String fileName = sd.getFileName();
if (fileName == null)
return;
String fileDir = sd.getDirectory();
FileInfo fi = imp.getOriginalFileInfo();
if (fi!=null && imp.getStack().isVirtual() && fileDir.equals(fi.directory) && fileName.equals(fi.fileName)) {
IJ.error("AVI Writer", "Virtual stacks cannot be saved in place.");
return;
}
try {
writeImage(imp, fileDir + fileName, COMPRESSION_TYPES[compressionIndex], jpegQuality);
IJ.showStatus("");
} catch (IOException e) {
IJ.error("AVI Writer", "An error occured writing the file.\n \n" + e);
}
IJ.showStatus("");
}
private boolean showDialog(ImagePlus imp) {
String options = Macro.getOptions();
if (options!=null) {
if (!options.contains("compression="))
options = "compression=JPEG "+options;
options = options.replace("compression=Uncompressed", "compression=None");
Macro.setOptions(options);
}
double fps = getFrameRate(imp);
int decimalPlaces = (int) fps == fps?0:1;
GenericDialog gd = new GenericDialog("Save as AVI...");
gd.addChoice("Compression:", COMPRESSION_STRINGS, COMPRESSION_STRINGS[compressionIndex]);
gd.addNumericField("Frame Rate:", fps, decimalPlaces, 3, "fps");
gd.showDialog(); if (gd.wasCanceled()) return false;
compressionIndex = gd.getNextChoiceIndex();
fps = gd.getNextNumber();
if (fps<=0.5) fps = 0.5;
imp.getCalibration().fps = fps;
return true;
}
public void writeImage (ImagePlus imp, String path, int compression, int jpegQuality)
throws IOException {
if (compression!=NO_COMPRESSION && compression!=JPEG_COMPRESSION && compression!=PNG_COMPRESSION)
throw new IllegalArgumentException("Unsupported Compression 0x"+Integer.toHexString(compression));
this.biCompression = compression;
if (jpegQuality < 0) jpegQuality = 0;
if (jpegQuality > 100) jpegQuality = 100;
this.jpegQuality = jpegQuality;
File file = new File(path);
raFile = new RandomAccessFile(file, "rw");
raFile.setLength(0);
imp.startTiming();
boolean isComposite = imp.isComposite();
boolean isHyperstack = imp.isHyperStack();
boolean isOverlay = imp.getOverlay()!=null && !imp.getHideOverlay();
xDim = imp.getWidth(); yDim = imp.getHeight(); zDim = imp.getStackSize(); boolean saveFrames=false, saveSlices=false, saveChannels=false;
int channels = imp.getNChannels();
int slices = imp.getNSlices();
int frames = imp.getNFrames();
int channel = imp.getChannel();
int slice = imp.getSlice();
int frame = imp.getFrame();
if (isHyperstack || isComposite) {
if (frames>1) {
saveFrames = true;
zDim = frames;
} else if (slices>1) {
saveSlices = true;
zDim = slices;
} else if (channels>1) {
saveChannels = true;
zDim = channels;
} else
isHyperstack = false;
}
if (imp.getType()==ImagePlus.COLOR_RGB || isComposite || biCompression==JPEG_COMPRESSION || isOverlay)
bytesPerPixel = 3; else
bytesPerPixel = 1; boolean writeLUT = bytesPerPixel==1; linePad = 0;
int minLineLength = bytesPerPixel*xDim;
if (biCompression==NO_COMPRESSION && minLineLength%4!=0)
linePad = 4 - minLineLength%4; frameDataSize = (bytesPerPixel*xDim+linePad)*yDim;
int microSecPerFrame = (int)Math.round((1.0/getFrameRate(imp))*1.0e6);
int dwChunkId = biCompression==NO_COMPRESSION ? FOURCC_00db : FOURCC_00dc;
long sizeEstimate = bytesPerPixel*xDim*yDim*(long)zDim;
int nAvixChunksEstimate = (int)(sizeEstimate/JUNK_SIZE_THRESHOLD); endHeadPointer = 4096+((nAvixChunksEstimate*16+1000)/1024)*1024;
writeString("RIFF"); chunkSizeHere(); writeString("AVI "); writeString("LIST"); chunkSizeHere(); writeString("hdrl"); writeString("avih"); writeInt(0x38); writeInt(microSecPerFrame); writeInt(0); writeInt(0); writeInt(0x10); writeInt(zDim); writeInt(0); writeInt(1); writeInt(0); writeInt(xDim); writeInt(yDim); writeInt(0); writeInt(0);
writeInt(0);
writeInt(0);
writeString("LIST"); chunkSizeHere(); writeString("strl"); writeString("strh"); writeInt(56); writeString("vids"); writeString("DIB "); writeInt(0); writeInt(0); writeInt(0); writeInt(1); writeInt((int)Math.round(getFrameRate(imp))); writeInt(0); writeInt(zDim); writeInt(0); writeInt(-1); writeInt(0); writeShort((short)0); writeShort((short)0); writeShort((short)0); writeShort((short)0); writeString("strf"); chunkSizeHere(); writeInt(40); writeInt(xDim); writeInt(yDim); writeShort(1); writeShort((short)(8*bytesPerPixel)); writeInt(biCompression); int biSizeImage = (biCompression==NO_COMPRESSION)?0:xDim*yDim*bytesPerPixel;
writeInt(biSizeImage); writeInt(0); writeInt(0); writeInt(writeLUT ? 256:0); writeInt(0); if (writeLUT) writeLUT(imp.getProcessor());
chunkEndWriteSize();
writeString("strn"); writeInt(16); writeString("ImageJ AVI \0"); pointer2indx = raFile.getFilePointer();
writeString("indx"); chunkSizeHere(); writeShort(4); writeByte(0); writeByte(0); pointer2indxNEntriesInUse = raFile.getFilePointer();
writeInt(0); writeInt(dwChunkId); writeInt(0); writeInt(0); writeInt(0); pointer2indxNextEntry = raFile.getFilePointer();
chunkEndWriteSize(); writeString("JUNK"); chunkSizeHere(); raFile.seek(endHeadPointer); chunkEndWriteSize(); chunkEndWriteSize(); chunkEndWriteSize();
if (biCompression == NO_COMPRESSION)
bufferWrite = new byte[frameDataSize];
else
raOutputStream = new RaOutputStream(raFile); int[] dataChunkOffset = new int[zDim]; int[] dataChunkLength = new int[zDim];
int currentFilePart = 0;
boolean writeAVI2index = false; int iFrame = 0;
while (iFrame < zDim) {
if (currentFilePart > 0) { writeString("RIFF");
chunkSizeHere(); writeString("AVIX"); }
writeString("LIST"); chunkSizeHere(); long moviPointer = raFile.getFilePointer();
writeString("movi");
int firstFrameInChunk = iFrame;
while (iFrame<zDim) {
if (iFrame %10==0) {
IJ.showProgress(iFrame, zDim);
IJ.showStatus(iFrame+"/"+zDim);
}
ImageProcessor ip = null; if (isComposite || isHyperstack || isOverlay) {
if (saveFrames)
imp.setPositionWithoutUpdate(channel, slice, iFrame+1);
else if (saveSlices)
imp.setPositionWithoutUpdate(channel, iFrame+1, frame);
else if (saveChannels)
imp.setPositionWithoutUpdate(iFrame+1, slice, frame);
ImagePlus imp2 = imp;
if (isOverlay) {
if (!(saveFrames||saveSlices||saveChannels))
imp.setSliceWithoutUpdate(iFrame+1);
imp2 = imp.flatten();
}
ip = new ColorProcessor(imp2.getImage());
} else
ip = zDim==1 ? imp.getProcessor() : imp.getStack().getProcessor(iFrame+1);
int chunkPointer = (int)raFile.getFilePointer();
writeInt(dwChunkId); chunkSizeHere(); if (biCompression == NO_COMPRESSION) {
if (bytesPerPixel==1)
writeByteFrame(ip);
else
writeRGBFrame(ip);
} else
writeCompressedFrame(ip);
dataChunkOffset[iFrame] = (int)(chunkPointer - moviPointer);
dataChunkLength[iFrame] = (int)(raFile.getFilePointer() - chunkPointer - 8); chunkEndWriteSize(); iFrame++;
if (raFile.getFilePointer() - moviPointer > JUNK_SIZE_THRESHOLD)
break; } int nFramesInChunk = iFrame - firstFrameInChunk;
if (iFrame < zDim)
writeAVI2index = true; if (writeAVI2index) {
long ix00pointer = raFile.getFilePointer();
writeString("ix00"); chunkSizeHere(); writeShort(2); writeByte(0); writeByte(1); writeInt(nFramesInChunk); writeInt(dwChunkId); writeLong(moviPointer); writeInt(0); for (int z=firstFrameInChunk; z<iFrame; z++) {
writeInt(dataChunkOffset[z]+8); writeInt(dataChunkLength[z]); }
writeMainIndxEntry(ix00pointer, (int)(raFile.getFilePointer()-ix00pointer), nFramesInChunk);
chunkEndWriteSize(); }
chunkEndWriteSize();
if (currentFilePart == 0) {
writeString("idx1"); chunkSizeHere(); for (int z = 0; z < iFrame; z++) {
writeInt(dwChunkId); writeInt(0x10); writeInt(dataChunkOffset[z]); writeInt(dataChunkLength[z]); } chunkEndWriteSize(); }
chunkEndWriteSize(); currentFilePart++;
}
if (!writeAVI2index) { raFile.seek(pointer2indx);
writeString("JUNK"); chunkSizeHere(); raFile.seek(endHeadPointer); chunkEndWriteSize(); }
raFile.close();
IJ.showProgress(1.0);
if (isComposite || isHyperstack)
imp.setPosition(channel, slice, frame);
}
private void chunkSizeHere() throws IOException {
sizePointers[stackPointer] = raFile.getFilePointer();
writeInt(0); stackPointer++;
}
private void chunkEndWriteSize() throws IOException {
stackPointer--;
long position = raFile.getFilePointer();
raFile.seek(sizePointers[stackPointer]);
writeInt((int)(position - (sizePointers[stackPointer]+4)));
raFile.seek(((position+1)/2)*2); }
private void writeMainIndxEntry(long ix00pointer, int dwSize, int nFrames) throws IOException {
if (pointer2indxNextEntry + 16 + 8 > MAX_INDX_SIZE) {
raFile.close();
throw new RuntimeException("AVI_Writer ERROR: Index Size Overflow");
}
long savePosition = raFile.getFilePointer();
raFile.seek(pointer2indxNextEntry);
writeLong(ix00pointer);
writeInt(dwSize);
writeInt(nFrames);
pointer2indxNextEntry += 16;
nIndxEntries++;
writeString("JUNK"); chunkSizeHere(); raFile.seek(endHeadPointer); chunkEndWriteSize(); raFile.seek(pointer2indx+4);
writeInt((int)(pointer2indxNextEntry - pointer2indx - 8)); raFile.seek(pointer2indxNEntriesInUse);
writeInt(nIndxEntries); raFile.seek(savePosition);
}
private void writeByteFrame(ImageProcessor ip) throws IOException {
ip = ip.convertToByte(true);
byte[] pixels = (byte[])ip.getPixels();
int width = ip.getWidth();
int height = ip.getHeight();
int c, offset, index = 0;
for (int y=height-1; y>=0; y--) {
offset = y*width;
for (int x=0; x<width; x++)
bufferWrite[index++] = pixels[offset++];
for (int i = 0; i<linePad; i++)
bufferWrite[index++] = (byte)0;
}
raFile.write(bufferWrite);
}
private void writeRGBFrame(ImageProcessor ip) throws IOException {
ip = ip.convertToRGB();
int[] pixels = (int[])ip.getPixels();
int width = ip.getWidth();
int height = ip.getHeight();
int c, offset, index = 0;
for (int y=height-1; y>=0; y--) {
offset = y*width;
for (int x=0; x<width; x++) {
c = pixels[offset++];
bufferWrite[index++] = (byte)(c&0xff); bufferWrite[index++] = (byte)((c&0xff00)>>8); bufferWrite[index++] = (byte)((c&0xff0000)>>16); }
for (int i = 0; i<linePad; i++)
bufferWrite[index++] = (byte)0;
}
raFile.write(bufferWrite);
}
private void writeCompressedFrame(ImageProcessor ip) throws IOException {
if (biCompression==JPEG_COMPRESSION) {
BufferedImage bi = getBufferedImage(ip);
ImageIO.write(bi, "jpeg", raOutputStream);
} else { BufferedImage bi = ip.getBufferedImage();
ImageIO.write(bi, "png", raOutputStream);
}
}
private BufferedImage getBufferedImage(ImageProcessor ip) {
BufferedImage bi = new BufferedImage(ip.getWidth(), ip.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D)bi.getGraphics();
g.drawImage(ip.createImage(), 0, 0, null);
return bi;
}
private void writeLUT(ImageProcessor ip) throws IOException {
IndexColorModel cm = (IndexColorModel)(ip.getCurrentColorModel());
int mapSize = cm.getMapSize();
byte[] lutWrite = new byte[4*256];
for (int i = 0; i<256; i++) {
if (i<mapSize) {
lutWrite[4*i] = (byte)cm.getBlue(i);
lutWrite[4*i+1] = (byte)cm.getGreen(i);
lutWrite[4*i+2] = (byte)cm.getRed(i);
lutWrite[4*i+3] = (byte)0;
}
}
raFile.write(lutWrite);
}
private double getFrameRate(ImagePlus imp) {
double rate = imp.getCalibration().fps;
if (rate==0.0)
rate = Animator.getFrameRate();
if (rate<=0.5) rate = 0.5;
return rate;
}
private void writeString(String s) throws IOException {
byte[] bytes = s.getBytes("UTF8");
raFile.write(bytes);
}
private void writeLong(long v) throws IOException {
for (int i=0; i<8; i++) {
raFile.write((int)(v & 0xFFL));
v = v>>>8;
}
}
private void writeInt(int v) throws IOException {
raFile.write(v & 0xFF);
raFile.write((v >>> 8) & 0xFF);
raFile.write((v >>> 16) & 0xFF);
raFile.write((v >>> 24) & 0xFF);
}
private void writeShort(int v) throws IOException {
raFile.write(v & 0xFF);
raFile.write((v >>> 8) & 0xFF);
}
private void writeByte(int v) throws IOException {
raFile.write(v & 0xFF);
}
class RaOutputStream extends OutputStream {
RandomAccessFile raFile;
RaOutputStream (RandomAccessFile raFile) {
this.raFile = raFile;
}
public void write (int b) throws IOException {
raFile.writeByte(b); }
public void write (byte[] b) throws IOException {
raFile.write(b);
}
public void write (byte[] b, int off, int len) throws IOException {
raFile.write(b, off, len);
}
}
}