//
// AVIWriter.java
//
/*
LOCI Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan,
Eric Kjellman and Brian Loranger.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
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 Library General Public License for more details.
You should have received a copy of the GNU Library 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
*/
package loci.formats.out;
import java.awt.Image;
import java.awt.image.*;
import java.io.*;
import java.util.Vector;
import loci.formats.*;
/**
* AVIWriter is the file format writer for AVI files.
*
* Much of this writer's code was adapted from Wayne Rasband's
* AVI Movie Writer plugin for ImageJ
* (available at http://rsb.info.nih.gov/ij/).
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/out/AVIWriter.java">Trac</a>,
* <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/out/AVIWriter.java">SVN</a></dd></dl>
*/
public class AVIWriter extends FormatWriter {
// -- Fields --
private RandomAccessFile raFile;
private int planesWritten = 0;
private int bytesPerPixel;
private File file;
private int xDim, yDim, zDim, tDim, xPad;
private int microSecPerFrame;
// location of file size in bytes not counting first 8 bytes
private long saveFileSize;
// location of length of CHUNK with first LIST - not including first 8
// bytes with LIST and size. JUNK follows the end of this CHUNK
private long saveLIST1Size;
// location of length of CHUNK with second LIST - not including first 8
// bytes with LIST and size. Note that saveLIST1subSize = saveLIST1Size +
// 76, and that the length size written to saveLIST2Size is 76 less than
// that written to saveLIST1Size. JUNK follows the end of this CHUNK.
private long saveLIST1subSize;
// location of length of strf CHUNK - not including the first 8 bytes with
// strf and size. strn follows the end of this CHUNK.
private long savestrfSize;
private byte[] text;
private long savestrnPos;
private long saveJUNKsignature;
private int paddingBytes;
private long saveLIST2Size;
private byte[] dataSignature;
private Vector savedbLength;
private long idx1Pos;
private long endPos;
private long saveidx1Length;
private int z;
private long savemovi;
private int xMod;
private long frameOffset;
private long frameOffset2;
// -- Constructor --
public AVIWriter() { super("Audio Video Interleave", "avi"); }
// -- IFormatWriter API methods --
/* @see loci.formats.IFormatWriter#saveImage(Image, boolean) */
public void saveImage(Image image, boolean last)
throws FormatException, IOException
{
if (image == null) {
throw new FormatException("Image is null");
}
BufferedImage img = null;
if (cm != null) img = ImageTools.makeBuffered(image, cm);
else img = ImageTools.makeBuffered(image);
byte[][] byteData = ImageTools.getBytes(img);
if (!initialized) {
initialized = true;
planesWritten = 0;
bytesPerPixel = byteData.length;
file = new File(currentId);
raFile = new RandomAccessFile(file, "rw");
raFile.seek(raFile.length());
saveFileSize = 4;
saveLIST1Size = 16;
saveLIST1subSize = 23 * 4;
frameOffset = 48;
frameOffset2 = 35 * 4;
savestrfSize = 42 * 4;
savestrnPos = savestrfSize + 44 + (bytesPerPixel == 1 ? 4 * 256 : 0);
saveJUNKsignature = savestrnPos + 24;
saveLIST2Size = 4088;
savemovi = 4092;
savedbLength = new Vector();
dataSignature = new byte[4];
dataSignature[0] = 48; // 0
dataSignature[1] = 48; // 0
dataSignature[2] = 100; // d
dataSignature[3] = 98; // b
tDim = 1;
zDim = 1;
yDim = img.getHeight();
xDim = img.getWidth();
xPad = 0;
xMod = xDim % 4;
if (xMod != 0) {
xPad = 4 - xMod;
xDim += xPad;
}
if (raFile.length() == 0) {
DataTools.writeString(raFile, "RIFF"); // signature
// Bytes 4 thru 7 contain the length of the file. This length does
// not include bytes 0 thru 7.
DataTools.writeInt(raFile, 0, true); // for now write 0 for size
DataTools.writeString(raFile, "AVI "); // RIFF type
// Write the first LIST chunk, which contains
// information on data decoding
DataTools.writeString(raFile, "LIST"); // CHUNK signature
// Write the length of the LIST CHUNK not including the first 8 bytes
// with LIST and size. Note that the end of the LIST CHUNK is followed
// by JUNK.
DataTools.writeInt(raFile, (bytesPerPixel == 1) ? 1240 : 216, true);
DataTools.writeString(raFile, "hdrl"); // CHUNK type
DataTools.writeString(raFile, "avih"); // Write the avih sub-CHUNK
// Write the length of the avih sub-CHUNK (38H) not including the
// the first 8 bytes for avihSignature and the length
DataTools.writeInt(raFile, 0x38, true);
// dwMicroSecPerFrame - Write the microseconds per frame
microSecPerFrame = (int) (1.0 / fps * 1.0e6);
DataTools.writeInt(raFile, microSecPerFrame, true);
// Write the maximum data rate of the file in bytes per second
DataTools.writeInt(raFile, 0, true); // dwMaxBytesPerSec
DataTools.writeInt(raFile, 0, true); // dwReserved1 - set to 0
// dwFlags - just set the bit for AVIF_HASINDEX
DataTools.writeInt(raFile, 0x10, true);
// 10H AVIF_HASINDEX: The AVI file has an idx1 chunk containing
// an index at the end of the file. For good performance, all
// AVI files should contain an index.
// 20H AVIF_MUSTUSEINDEX: Index CHUNK, rather than the physical
// ordering of the chunks in the file, must be used to determine the
// order of the frames.
// 100H AVIF_ISINTERLEAVED: Indicates that the AVI file is interleaved.
// This is used to read data from a CD-ROM more efficiently.
// 800H AVIF_TRUSTCKTYPE: USE CKType to find key frames
// 10000H AVIF_WASCAPTUREFILE: The AVI file is used for capturing
// real-time video. Applications should warn the user before
// writing over a file with this fla set because the user
// probably defragmented this file.
// 20000H AVIF_COPYRIGHTED: The AVI file contains copyrighted data
// and software. When, this flag is used, software should not
// permit the data to be duplicated.
// dwTotalFrames - total frame number
DataTools.writeInt(raFile, zDim * tDim, true);
// dwInitialFrames -Initial frame for interleaved files.
// Noninterleaved files should specify 0.
DataTools.writeInt(raFile, 0, true);
// dwStreams - number of streams in the file - here 1 video and
// zero audio.
DataTools.writeInt(raFile, 1, true);
// dwSuggestedBufferSize - Suggested buffer size for reading the file.
// Generally, this size should be large enough to contain the largest
// chunk in the file.
DataTools.writeInt(raFile, 0, true);
// dwWidth - image width in pixels
DataTools.writeInt(raFile, xDim - xPad, true);
DataTools.writeInt(raFile, yDim, true); // dwHeight - height in pixels
// dwReserved[4] - Microsoft says to set the following 4 values to 0.
DataTools.writeInt(raFile, 0, true);
DataTools.writeInt(raFile, 0, true);
DataTools.writeInt(raFile, 0, true);
DataTools.writeInt(raFile, 0, true);
// Write the Stream line header CHUNK
DataTools.writeString(raFile, "LIST");
// Write the size of the first LIST subCHUNK not including the first 8
// bytes with LIST and size. Note that saveLIST1subSize = saveLIST1Size
// + 76, and that the length written to saveLIST1subSize is 76 less than
// the length written to saveLIST1Size. The end of the first LIST
// subCHUNK is followed by JUNK.
DataTools.writeInt(raFile, (bytesPerPixel == 1) ? 1164 : 140 , true);
DataTools.writeString(raFile, "strl"); // Write the chunk type
DataTools.writeString(raFile, "strh"); // Write the strh sub-CHUNK
DataTools.writeInt(raFile, 56, true); // Write length of strh sub-CHUNK
// fccType - Write the type of data stream - here vids for video stream
DataTools.writeString(raFile, "vids");
// Write DIB for Microsoft Device Independent Bitmap.
// Note: Unfortunately, at least 3 other four character codes are
// sometimes used for uncompressed AVI videos: 'RGB ', 'RAW ',
// 0x00000000
DataTools.writeString(raFile, "DIB ");
DataTools.writeInt(raFile, 0, true); // dwFlags
// 0x00000001 AVISF_DISABLED The stram data should be rendered only when
// explicitly enabled.
// 0x00010000 AVISF_VIDEO_PALCHANGES Indicates that a palette change is
// included in the AVI file. This flag warns the playback software that
// it will need to animate the palette.
// dwPriority - priority of a stream type. For example, in a file with
// multiple audio streams, the one with the highest priority might be
// the default one.
DataTools.writeInt(raFile, 0, true);
// dwInitialFrames - Specifies how far audio data is skewed ahead of
// video frames in interleaved files. Typically, this is about 0.75
// seconds. In interleaved files specify the number of frames in the
// file prior to the initial frame of the AVI sequence.
// Noninterleaved files should use zero.
DataTools.writeInt(raFile, 0, true);
// rate/scale = samples/second
DataTools.writeInt(raFile, 1, true); // dwScale
// dwRate - frame rate for video streams
DataTools.writeInt(raFile, fps, true);
// dwStart - this field is usually set to zero
DataTools.writeInt(raFile, 0, true);
// dwLength - playing time of AVI file as defined by scale and rate
// Set equal to the number of frames
DataTools.writeInt(raFile, tDim * zDim, true);
// dwSuggestedBufferSize - Suggested buffer size for reading the stream.
// Typically, this contains a value corresponding to the largest chunk
// in a stream.
DataTools.writeInt(raFile, 0, true);
// dwQuality - encoding quality given by an integer between 0 and
// 10,000. If set to -1, drivers use the default quality value.
DataTools.writeInt(raFile, -1, true);
// dwSampleSize #
// 0 if the video frames may or may not vary in size
// If 0, each sample of data(such as a video frame) must be in a
// separate chunk. If nonzero, then multiple samples of data can be
// grouped into a single chunk within the file.
DataTools.writeInt(raFile, 0, true);
// rcFrame - Specifies the destination rectangle for a text or video
// stream within the movie rectangle specified by the dwWidth and
// dwHeight members of the AVI main header structure. The rcFrame member
// is typically used in support of multiple video streams. Set this
// rectangle to the coordinates corresponding to the movie rectangle to
// update the whole movie rectangle. Units for this member are pixels.
// The upper-left corner of the destination rectangle is relative to the
// upper-left corner of the movie rectangle.
DataTools.writeShort(raFile, (short) 0, true); // left
DataTools.writeShort(raFile, (short) 0, true); // top
DataTools.writeShort(raFile, (short) 0, true); // right
DataTools.writeShort(raFile, (short) 0, true); // bottom
// Write the size of the stream format CHUNK not including the first 8
// bytes for strf and the size. Note that the end of the stream format
// CHUNK is followed by strn.
DataTools.writeString(raFile, "strf"); // Write the stream format chunk
// write the strf CHUNK size
DataTools.writeInt(raFile, (bytesPerPixel == 1) ? 1068 : 44, true);
// Applications should use this size to determine which BITMAPINFO
// header structure is being used. This size includes this biSize field.
// biSize- Write header size of BITMAPINFO header structure
DataTools.writeInt(raFile, 40, true);
// biWidth - image width in pixels
DataTools.writeInt(raFile, xDim, true);
// biHeight - image height in pixels. If height is positive, the bitmap
// is a bottom up DIB and its origin is in the lower left corner. If
// height is negative, the bitmap is a top-down DIB and its origin is
// the upper left corner. This negative sign feature is supported by the
// Windows Media Player, but it is not supported by PowerPoint.
DataTools.writeInt(raFile, yDim, true);
// biPlanes - number of color planes in which the data is stored
// This must be set to 1.
DataTools.writeShort(raFile, 1, true);
int bitsPerPixel = (bytesPerPixel == 3) ? 24 : 8;
// biBitCount - number of bits per pixel #
// 0L for BI_RGB, uncompressed data as bitmap
DataTools.writeShort(raFile, (short) bitsPerPixel, true);
//writeInt(bytesPerPixel * xDim * yDim * zDim * tDim); // biSizeImage #
DataTools.writeInt(raFile, 0, true); // biSizeImage #
DataTools.writeInt(raFile, 0, true); // biCompression - compression type
// biXPelsPerMeter - horizontal resolution in pixels
DataTools.writeInt(raFile, 0, true);
// biYPelsPerMeter - vertical resolution in pixels per meter
DataTools.writeInt(raFile, 0, true);
if (bitsPerPixel == 8) DataTools.writeInt(raFile, 256, true);
else DataTools.writeInt(raFile, 0, true); // biClrUsed
// biClrImportant - specifies that the first x colors of the color table
// are important to the DIB. If the rest of the colors are not
// available, the image still retains its meaning in an acceptable
// manner. When this field is set to zero, all the colors are important,
// or, rather, their relative importance has not been computed.
DataTools.writeInt(raFile, 0, true);
// Write the LUTa.getExtents()[1] color table entries here. They are
// written: blue byte, green byte, red byte, 0 byte
if (bytesPerPixel == 1) {
byte[] lutWrite = new byte[4 * 256];
for (int i=0; i<256; i++) {
lutWrite[4*i] = (byte) i; // blue
lutWrite[4*i+1] = (byte) i; // green
lutWrite[4*i+2] = (byte) i; // red
lutWrite[4*i+3] = 0;
}
raFile.write(lutWrite);
}
raFile.seek(savestrfSize);
DataTools.writeInt(raFile,
(int) (savestrnPos - (savestrfSize + 4)), true);
raFile.seek(savestrnPos);
// Use strn to provide zero terminated text string describing the stream
DataTools.writeString(raFile, "strn");
DataTools.writeInt(raFile, 16, true); // Write length of strn sub-CHUNK
text = new byte[16];
text[0] = 70; // F
text[1] = 105; // i
text[2] = 108; // l
text[3] = 101; // e
text[4] = 65; // A
text[5] = 86; // V
text[6] = 73; // I
text[7] = 32; // space
text[8] = 119; // w
text[9] = 114; // r
text[10] = 105; // i
text[11] = 116; // t
text[12] = 101; // e
text[13] = 32; // space
text[14] = 32; // space
text[15] = 0; // termination byte
raFile.write(text);
raFile.seek(saveLIST1Size);
DataTools.writeInt(raFile,
(int) (saveJUNKsignature - (saveLIST1Size + 4)), true);
raFile.seek(saveLIST1subSize);
DataTools.writeInt(raFile,
(int) (saveJUNKsignature - (saveLIST1subSize + 4)), true);
raFile.seek(saveJUNKsignature);
// write a JUNK CHUNK for padding
DataTools.writeString(raFile, "JUNK");
paddingBytes = (int) (4084 - (saveJUNKsignature + 8));
DataTools.writeInt(raFile, paddingBytes, true);
for (int i=0; i<paddingBytes/2; i++) {
DataTools.writeShort(raFile, (short) 0, true);
}
// Write the second LIST chunk, which contains the actual data
DataTools.writeString(raFile, "LIST");
// Write the length of the LIST CHUNK not including the first 8 bytes
// with LIST and size. The end of the second LIST CHUNK is followed by
// idx1.
saveLIST2Size = raFile.getFilePointer();
DataTools.writeInt(raFile, 0, true); // For now write 0
DataTools.writeString(raFile, "movi"); // Write CHUNK type 'movi'
}
}
// Write the data. Each 3-byte triplet in the bitmap array represents the
// relative intensities of blue, green, and red, respectively, for a pixel.
// The color bytes are in reverse order from the Windows convention.
int width = xDim - xPad;
int height = byteData[0].length / width;
raFile.write(dataSignature);
savedbLength.add(new Long(raFile.getFilePointer()));
// Write the data length
DataTools.writeInt(raFile, bytesPerPixel * xDim * yDim, true);
if (bytesPerPixel == 1) {
for (int i=(height-1); i>=0; i--) {
raFile.write(byteData[0], i*width, width);
for (int j=0; j<xPad; j++) raFile.write(0);
}
}
else {
byte[] buf = new byte[bytesPerPixel * xDim * yDim];
int offset = 0;
int next = 0;
for (int i=(height-1); i>=0; i--) {
for (int j=0; j<width; j++) {
offset = i*width + j;
for (int k=(byteData.length - 1); k>=0; k--) {
buf[next] = byteData[k][offset];
next++;
}
}
next += xPad * byteData.length;
}
raFile.write(buf);
buf = null;
}
planesWritten++;
if (last) {
// Write the idx1 CHUNK
// Write the 'idx1' signature
idx1Pos = raFile.getFilePointer();
raFile.seek(saveLIST2Size);
DataTools.writeInt(raFile, (int) (idx1Pos - (saveLIST2Size + 4)), true);
raFile.seek(idx1Pos);
DataTools.writeString(raFile, "idx1");
saveidx1Length = raFile.getFilePointer();
// Write the length of the idx1 CHUNK not including the idx1 signature
DataTools.writeInt(raFile, 4 + (planesWritten*16), true);
for (z=0; z<planesWritten; z++) {
// In the ckid field write the 4 character code to identify the chunk
// 00db or 00dc
raFile.write(dataSignature);
// Write the flags - select AVIIF_KEYFRAME
if (z == 0) DataTools.writeInt(raFile, 0x10, true);
else DataTools.writeInt(raFile, 0x00, true);
// AVIIF_KEYFRAME 0x00000010L
// The flag indicates key frames in the video sequence.
// Key frames do not need previous video information to be
// decompressed.
// AVIIF_NOTIME 0x00000100L The CHUNK does not influence video timing
// (for example a palette change CHUNK).
// AVIIF_LIST 0x00000001L Marks a LIST CHUNK.
// AVIIF_TWOCC 2L
// AVIIF_COMPUSE 0x0FFF0000L These bits are for compressor use.
DataTools.writeInt(raFile, (int) (((Long)
savedbLength.get(z)).longValue() - 4 - savemovi), true);
// Write the offset (relative to the 'movi' field) to the relevant
// CHUNK. Write the length of the relevant CHUNK. Note that this length
// is also written at savedbLength
DataTools.writeInt(raFile, bytesPerPixel*xDim*yDim, true);
}
endPos = raFile.getFilePointer();
raFile.seek(saveFileSize);
DataTools.writeInt(raFile, (int) (endPos - (saveFileSize + 4)), true);
raFile.seek(saveidx1Length);
DataTools.writeInt(raFile, (int) (endPos - (saveidx1Length + 4)), true);
// write the total number of planes
raFile.seek(frameOffset);
DataTools.writeInt(raFile, planesWritten, true);
raFile.seek(frameOffset2);
DataTools.writeInt(raFile, planesWritten, true);
raFile.close();
}
}
/* @see loci.formats.IFormatWriter#canDoStacks() */
public boolean canDoStacks() { return true; }
/* @see loci.formats.IFormatWriter#getPixelTypes() */
public int[] getPixelTypes() {
return new int[] {FormatTools.UINT8};
}
// -- IFormatHandler API methods --
/* @see loci.formats.IFormatHandler#close() */
public void close() throws IOException {
if (raFile != null) raFile.close();
raFile = null;
currentId = null;
initialized = false;
}
}