//
// AVIWriter.java
//
/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.
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; 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 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
*/
package loci.formats.out;
import java.awt.image.IndexColorModel;
import java.io.IOException;
import java.util.Vector;
import loci.common.RandomAccessInputStream;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.FormatWriter;
import loci.formats.meta.MetadataRetrieve;
/**
* 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="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/out/AVIWriter.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/out/AVIWriter.java;hb=HEAD">Gitweb</a></dd></dl>
*/
public class AVIWriter extends FormatWriter {
// -- Constants --
private static final long SAVE_MOVI = 4092;
private static final long SAVE_FILE_SIZE = 4;
// location of length of strf CHUNK - not including the first 8 bytes with
// strf and size. strn follows the end of this CHUNK.
private static final long SAVE_STRF_SIZE = 168;
private static final long SAVE_STRN_POS = SAVE_STRF_SIZE + 1068;
private static final long SAVE_JUNK_SIG = SAVE_STRN_POS + 24;
// 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 static final long SAVE_LIST1_SIZE = 16;
// 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 static final long SAVE_LIST1_SUBSIZE = 92;
private static final long FRAME_OFFSET = 48;
private static final long FRAME_OFFSET_2 = 140;
private static final long PADDING_BYTES = 4076 - SAVE_JUNK_SIG;
private static final long SAVE_LIST2_SIZE = 4088;
private static final String DATA_SIGNATURE = "00db";
// -- Fields --
private int planesWritten = 0;
private int bytesPerPixel;
private int xDim, yDim, zDim, tDim, xPad;
private int microSecPerFrame;
private Vector<Long> savedbLength;
private long idx1Pos;
private long endPos;
private long saveidx1Length;
// -- Constructor --
public AVIWriter() { super("Audio Video Interleave", "avi"); }
// -- IFormatWriter API methods --
/**
* @see loci.formats.IFormatWriter#saveBytes(int, byte[], int, int, int, int)
*/
public void saveBytes(int no, byte[] buf, int x, int y, int w, int h)
throws FormatException, IOException
{
checkParams(no, buf, x, y, w, h);
if (!isFullPlane(x, y, w, h)) {
throw new FormatException(
"AVIWriter does not yet support saving image tiles.");
}
int nChannels = getSamplesPerPixel();
if (!initialized[series][no]) {
initialized[series][no] = true;
}
// 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 = buf.length / (width * bytesPerPixel);
out.seek(idx1Pos);
out.writeBytes(DATA_SIGNATURE);
savedbLength.add(new Long(out.getFilePointer()));
// Write the data length
out.writeInt(bytesPerPixel * xDim * yDim);
int rowPad = xPad * bytesPerPixel;
byte[] rowBuffer = new byte[width * bytesPerPixel + rowPad];
for (int row=height-1; row>=0; row--) {
for (int col=0; col<width; col++) {
int offset = row * width + col;
if (interleaved) offset *= nChannels;
byte r = buf[offset];
if (nChannels > 1) {
byte g = buf[offset + (interleaved ? 1 : width * height)];
byte b = 0;
if (nChannels > 2) {
b = buf[offset + (interleaved ? 2 : 2 * width * height)];
}
rowBuffer[col * bytesPerPixel] = b;
rowBuffer[col * bytesPerPixel + 1] = g;
}
rowBuffer[col * bytesPerPixel + bytesPerPixel - 1] = r;
}
out.write(rowBuffer);
}
planesWritten++;
// Write the idx1 CHUNK
// Write the 'idx1' signature
idx1Pos = out.getFilePointer();
out.seek(SAVE_LIST2_SIZE);
out.writeInt((int) (idx1Pos - (SAVE_LIST2_SIZE + 4)));
out.seek(idx1Pos);
out.writeBytes("idx1");
saveidx1Length = out.getFilePointer();
// Write the length of the idx1 CHUNK not including the idx1 signature
out.writeInt(4 + (planesWritten*16));
for (int z=0; z<planesWritten; z++) {
// In the ckid field write the 4 character code to identify the chunk
// 00db or 00dc
out.writeBytes(DATA_SIGNATURE);
// Write the flags - select AVIIF_KEYFRAME
if (z == 0) out.writeInt(0x10);
else out.writeInt(0x00);
// 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.
out.writeInt((int) (savedbLength.get(z) - 4 - SAVE_MOVI));
// 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
out.writeInt(bytesPerPixel*xDim*yDim);
}
endPos = out.getFilePointer();
out.seek(SAVE_FILE_SIZE);
out.writeInt((int) (endPos - (SAVE_FILE_SIZE + 4)));
out.seek(saveidx1Length);
out.writeInt((int) (endPos - (saveidx1Length + 4)));
// write the total number of planes
out.seek(FRAME_OFFSET);
out.writeInt(planesWritten);
out.seek(FRAME_OFFSET_2);
out.writeInt(planesWritten);
}
/* @see loci.formats.IFormatWriter#canDoStacks() */
public boolean canDoStacks() { return true; }
/* @see loci.formats.IFormatWriter#getPixelTypes(String) */
public int[] getPixelTypes(String codec) {
return new int[] {FormatTools.UINT8};
}
// -- IFormatHandler API methods --
/* @see loci.formats.IFormatHandler#close() */
public void close() throws IOException {
super.close();
planesWritten = 0;
bytesPerPixel = 0;
xDim = yDim = zDim = tDim = xPad = 0;
microSecPerFrame = 0;
savedbLength = null;
idx1Pos = 0;
endPos = 0;
saveidx1Length = 0;
}
/* @see loci.formats.IFormatHandler#setId(String) */
public void setId(String id) throws FormatException, IOException {
super.setId(id);
savedbLength = new Vector<Long>();
if (out.length() > 0) {
RandomAccessInputStream in = new RandomAccessInputStream(currentId);
in.order(true);
in.seek(FRAME_OFFSET);
planesWritten = in.readInt();
in.seek(SAVE_FILE_SIZE);
endPos = in.readInt() + SAVE_FILE_SIZE + 4;
in.seek(SAVE_LIST2_SIZE);
idx1Pos = in.readInt() + SAVE_LIST2_SIZE + 4;
saveidx1Length = idx1Pos + 4;
if (planesWritten > 0) in.seek(saveidx1Length + 4);
for (int z=0; z<planesWritten; z++) {
in.skipBytes(8);
savedbLength.add(in.readInt() + 4 + SAVE_MOVI);
in.skipBytes(4);
}
in.close();
out.seek(idx1Pos);
}
out.order(true);
MetadataRetrieve meta = getMetadataRetrieve();
tDim = meta.getPixelsSizeZ(series).getValue().intValue();
zDim = meta.getPixelsSizeT(series).getValue().intValue();
yDim = meta.getPixelsSizeY(series).getValue().intValue();
xDim = meta.getPixelsSizeX(series).getValue().intValue();
String type = meta.getPixelsType(series).toString();
int pixelType = FormatTools.pixelTypeFromString(type);
bytesPerPixel = FormatTools.getBytesPerPixel(pixelType);
bytesPerPixel *= getSamplesPerPixel();
xPad = 0;
int xMod = xDim % 4;
if (xMod != 0) {
xPad = 4 - xMod;
xDim += xPad;
}
byte[][] lut = null;
if (getColorModel() instanceof IndexColorModel) {
lut = new byte[4][256];
IndexColorModel model = (IndexColorModel) getColorModel();
model.getReds(lut[0]);
model.getGreens(lut[1]);
model.getBlues(lut[2]);
model.getAlphas(lut[3]);
}
if (out.length() == 0) {
out.writeBytes("RIFF"); // signature
// Bytes 4 thru 7 contain the length of the file. This length does
// not include bytes 0 thru 7.
out.writeInt(0); // for now write 0 for size
out.writeBytes("AVI "); // RIFF type
// Write the first LIST chunk, which contains
// information on data decoding
out.writeBytes("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.
out.writeInt((bytesPerPixel == 1) ? 1240 : 216);
out.writeBytes("hdrl"); // CHUNK type
out.writeBytes("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
out.writeInt(0x38);
// dwMicroSecPerFrame - Write the microseconds per frame
microSecPerFrame = (int) (1.0 / fps * 1.0e6);
out.writeInt(microSecPerFrame);
// Write the maximum data rate of the file in bytes per second
out.writeInt(0); // dwMaxBytesPerSec
out.writeInt(0); // dwReserved1 - set to 0
// dwFlags - just set the bit for AVIF_HASINDEX
out.writeInt(0x10);
// 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
out.writeInt(0);
// dwInitialFrames -Initial frame for interleaved files.
// Noninterleaved files should specify 0.
out.writeInt(0);
// dwStreams - number of streams in the file - here 1 video and
// zero audio.
out.writeInt(1);
// dwSuggestedBufferSize - Suggested buffer size for reading the file.
// Generally, this size should be large enough to contain the largest
// chunk in the file.
out.writeInt(0);
// dwWidth - image width in pixels
out.writeInt(xDim - xPad);
out.writeInt(yDim); // dwHeight - height in pixels
// dwReserved[4] - Microsoft says to set the following 4 values to 0.
out.writeInt(0);
out.writeInt(0);
out.writeInt(0);
out.writeInt(0);
// Write the Stream line header CHUNK
out.writeBytes("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.
out.writeInt((bytesPerPixel == 1) ? 1164 : 140);
out.writeBytes("strl"); // Write the chunk type
out.writeBytes("strh"); // Write the strh sub-CHUNK
out.writeInt(56); // Write length of strh sub-CHUNK
// fccType - Write the type of data stream - here vids for video stream
out.writeBytes("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
out.writeBytes("DIB ");
out.writeInt(0); // 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.
out.writeInt(0);
// 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.
out.writeInt(0);
// rate/scale = samples/second
out.writeInt(1); // dwScale
// dwRate - frame rate for video streams
out.writeInt(fps);
// dwStart - this field is usually set to zero
out.writeInt(0);
// dwLength - playing time of AVI file as defined by scale and rate
// Set equal to the number of frames
out.writeInt(tDim * zDim);
// dwSuggestedBufferSize - Suggested buffer size for reading the stream.
// Typically, this contains a value corresponding to the largest chunk
// in a stream.
out.writeInt(0);
// dwQuality - encoding quality given by an integer between 0 and
// 10,000. If set to -1, drivers use the default quality value.
out.writeInt(-1);
// 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.
out.writeInt(0);
// 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.
out.writeShort((short) 0); // left
out.writeShort((short) 0); // top
out.writeShort((short) 0); // right
out.writeShort((short) 0); // 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.
out.writeBytes("strf"); // Write the stream format chunk
// write the strf CHUNK size
out.writeInt((bytesPerPixel == 1) ? 1068 : 44);
// 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
out.writeInt(40);
// biWidth - image width in pixels
out.writeInt(xDim);
// 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.
out.writeInt(yDim);
// biPlanes - number of color planes in which the data is stored
// This must be set to 1.
out.writeShort(1);
int bitsPerPixel = (bytesPerPixel == 3) ? 24 : 8;
// biBitCount - number of bits per pixel #
// 0L for BI_RGB, uncompressed data as bitmap
out.writeShort((short) bitsPerPixel);
out.writeInt(0); // biSizeImage #
out.writeInt(0); // biCompression - compression type
// biXPelsPerMeter - horizontal resolution in pixels
out.writeInt(0);
// biYPelsPerMeter - vertical resolution in pixels per meter
out.writeInt(0);
int nColors = 256;
out.writeInt(nColors);
// 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.
out.writeInt(0);
// Write the LUTa.getExtents()[1] color table entries here. They are
// written: blue byte, green byte, red byte, 0 byte
if (bytesPerPixel == 1) {
if (lut != null) {
for (int i=0; i<256; i++) {
out.write(lut[2][i]);
out.write(lut[1][i]);
out.write(lut[0][i]);
out.write(lut[3][i]);
}
}
else {
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;
}
out.write(lutWrite);
}
}
out.seek(SAVE_STRF_SIZE);
out.writeInt((int) (SAVE_STRN_POS - (SAVE_STRF_SIZE + 4)));
out.seek(SAVE_STRN_POS);
// Use strn to provide zero terminated text string describing the stream
out.writeBytes("strn");
out.writeInt(16); // Write length of strn sub-CHUNK
out.writeBytes("FileAVI write ");
out.seek(SAVE_LIST1_SIZE);
out.writeInt((int) (SAVE_JUNK_SIG - (SAVE_LIST1_SIZE + 4)));
out.seek(SAVE_LIST1_SUBSIZE);
out.writeInt((int) (SAVE_JUNK_SIG - (SAVE_LIST1_SUBSIZE + 4)));
out.seek(SAVE_JUNK_SIG);
// write a JUNK CHUNK for padding
out.writeBytes("JUNK");
out.writeInt((int) PADDING_BYTES);
for (int i=0; i<PADDING_BYTES/2; i++) {
out.writeShort((short) 0);
}
// Write the second LIST chunk, which contains the actual data
out.writeBytes("LIST");
out.writeInt(4); // For now write 0
out.writeBytes("movi"); // Write CHUNK type 'movi'
idx1Pos = out.getFilePointer();
}
}
}