// // AVIReader.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.in; import java.io.IOException; import java.util.Vector; import loci.common.RandomAccessInputStream; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; import loci.formats.ImageTools; import loci.formats.MetadataTools; import loci.formats.UnsupportedCompressionException; import loci.formats.codec.BitBuffer; import loci.formats.codec.CodecOptions; import loci.formats.codec.JPEGCodec; import loci.formats.codec.MSRLECodec; import loci.formats.codec.MSVideoCodec; import loci.formats.meta.MetadataStore; /** * AVIReader is the file format reader for AVI files. * * Much of this code was adapted from Wayne Rasband's AVI Movie Reader * 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/in/AVIReader.java">Trac</a>, * <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/in/AVIReader.java;hb=HEAD">Gitweb</a></dd></dl> */ public class AVIReader extends FormatReader { // -- Constants -- public static final String AVI_MAGIC_STRING = "RIFF"; /** Supported compression types. */ private static final int MSRLE = 1; private static final int MS_VIDEO = 1296126531; //private static final int CINEPAK = 1684633187; private static final int JPEG = 1196444237; private static final int Y8 = 538982489; // -- Fields -- /** Offset to each plane. */ private Vector<Long> offsets; /** Number of bytes in each plane. */ private Vector<Long> lengths; private String listString; private String type = "error"; private String fcc = "error"; private int size = -1; private long pos; private int bytesPerPlane; // Stream Format chunk fields private int bmpColorsUsed, bmpWidth; private int bmpCompression, bmpScanLineSize; private short bmpBitsPerPixel; private byte[][] lut = null; private byte[] lastImage; private int lastImageNo; // -- Constructor -- /** Constructs a new AVI reader. */ public AVIReader() { super("Audio Video Interleave", "avi"); suffixNecessary = false; domains = new String[] {FormatTools.GRAPHICS_DOMAIN}; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ public boolean isThisType(RandomAccessInputStream stream) throws IOException { final int blockLen = 12; if (!FormatTools.validStream(stream, blockLen, false)) return false; String type = stream.readString(4); stream.skipBytes(4); String format = stream.readString(4); return type.equals(AVI_MAGIC_STRING) && format.equals("AVI "); } /* @see loci.formats.IFormatReader#get8BitLookupTable() */ public byte[][] get8BitLookupTable() { FormatTools.assertId(currentId, true, 1); return isRGB() ? null : lut; } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); int bytes = FormatTools.getBytesPerPixel(getPixelType()); double p = ((double) bmpScanLineSize) / bmpBitsPerPixel; int effectiveWidth = (int) (bmpScanLineSize / p); if (effectiveWidth == 0 || effectiveWidth < getSizeX()) { effectiveWidth = getSizeX(); } long fileOff = offsets.get(no).longValue(); long end = no < offsets.size() - 1 ? offsets.get(no + 1) : in.length(); long maxBytes = end - fileOff; in.seek(fileOff); if (bmpCompression != 0 && bmpCompression != Y8) { byte[] b = uncompress(no, buf); int rowLen = FormatTools.getPlaneSize(this, w, 1); int inputRowLen = FormatTools.getPlaneSize(this, getSizeX(), 1); for (int row=0; row<h; row++) { System.arraycopy(b, (row + y) * inputRowLen + x * bytes, buf, row * rowLen, rowLen); } b = null; return buf; } if (bmpBitsPerPixel < 8) { int rawSize = FormatTools.getPlaneSize(this, effectiveWidth, getSizeY()); rawSize /= (8 / bmpBitsPerPixel); byte[] b = new byte[rawSize]; int len = rawSize / getSizeY(); in.read(b); BitBuffer bb = new BitBuffer(b); bb.skipBits(bmpBitsPerPixel * len * (getSizeY() - h - y)); for (int row=h; row>=y; row--) { bb.skipBits(bmpBitsPerPixel * x); for (int col=0; col<len; col++) { buf[(row - y) * len + col] = (byte) bb.getBits(bmpBitsPerPixel); } bb.skipBits(bmpBitsPerPixel * (getSizeX() - w - x)); } return buf; } int pad = (bmpScanLineSize / getRGBChannelCount()) - getSizeX() * bytes; int scanline = w * bytes * (isInterleaved() ? getRGBChannelCount() : 1); in.skipBytes((getSizeX() + pad) * bytes * (getSizeY() - h - y)); if (getSizeX() == w && pad == 0) { for (int row=0; row<getSizeY(); row++) { int outputRow = bmpCompression == Y8 ? row : getSizeY() - row - 1; in.read(buf, outputRow * scanline, scanline); } // swap channels if (bmpBitsPerPixel == 24 || bmpBitsPerPixel == 32) { for (int i=0; i<buf.length / getRGBChannelCount(); i++) { byte r = buf[i * getRGBChannelCount() + 2]; buf[i * getRGBChannelCount() + 2] = buf[i * getRGBChannelCount()]; buf[i * getRGBChannelCount()] = r; } } } else { int skip = FormatTools.getPlaneSize(this, getSizeX() - w - x + pad, 1); if ((getSizeX() + pad) * getSizeY() * getRGBChannelCount() > maxBytes) { skip /= getRGBChannelCount(); } for (int i=h - 1; i>=0; i--) { in.skipBytes(x * (bmpBitsPerPixel / 8)); in.read(buf, (i - y)*scanline, scanline); if (bmpBitsPerPixel == 24) { for (int j=0; j<w; j++) { byte r = buf[i*scanline + j*3 + 2]; buf[i*scanline + j*3 + 2] = buf[i*scanline + j*3]; buf[i*scanline + j*3] = r; } } if (i > 0) in.skipBytes(skip); } } if (bmpBitsPerPixel == 16 && isRGB()) { // channels are stored as BGR, need to swap them ImageTools.bgrToRgb(buf, isInterleaved(), 2, getRGBChannelCount()); } return buf; } /* @see loci.formats.IFormatReader#close(boolean) */ public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (!fileOnly) { listString = null; offsets = null; lengths = null; type = null; fcc = null; size = -1; pos = 0; bytesPerPlane = 0; bmpColorsUsed = bmpWidth = bmpCompression = bmpScanLineSize = 0; bmpBitsPerPixel = 0; lut = null; lastImage = null; lastImageNo = -1; } } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ protected void initFile(String id) throws FormatException, IOException { super.initFile(id); in = new RandomAccessInputStream(id); in.order(true); LOGGER.info("Verifying AVI format"); offsets = new Vector<Long>(); lengths = new Vector<Long>(); lastImageNo = -1; while (in.getFilePointer() < in.length() - 8) { readChunk(); } LOGGER.info("Populating metadata"); core[0].imageCount = offsets.size(); core[0].sizeZ = 1; core[0].sizeT = getImageCount(); core[0].littleEndian = true; core[0].interleaved = bmpBitsPerPixel != 16; addGlobalMeta("Compression", getCodecName(bmpCompression)); if (bmpCompression == JPEG) { long fileOff = offsets.get(0).longValue(); in.seek(fileOff); int nBytes = uncompress(0, null).length / (getSizeX() * getSizeY()); if (bmpBitsPerPixel == 16) { nBytes /= 2; } core[0].sizeC = nBytes; core[0].rgb = getSizeC() > 1; } else if (bmpBitsPerPixel == 32) { core[0].sizeC = 4; core[0].rgb = true; } else if (bytesPerPlane == 0 || bmpBitsPerPixel == 24) { core[0].rgb = bmpBitsPerPixel > 8 || (bmpCompression != 0 && lut == null); core[0].sizeC = isRGB() ? 3 : 1; } else { core[0].sizeC = bytesPerPlane / (getSizeX() * getSizeY() * (bmpBitsPerPixel / 8)); core[0].rgb = getSizeC() > 1; } core[0].dimensionOrder = isRGB() ? "XYCTZ" : "XYTCZ"; core[0].falseColor = false; core[0].metadataComplete = true; core[0].indexed = lut != null && !isRGB(); if (bmpBitsPerPixel <= 8) { core[0].pixelType = FormatTools.UINT8; core[0].bitsPerPixel = bmpBitsPerPixel; } else if (bmpBitsPerPixel == 16) core[0].pixelType = FormatTools.UINT16; else if (bmpBitsPerPixel == 24 || bmpBitsPerPixel == 32) { core[0].pixelType = FormatTools.UINT8; } else { throw new FormatException( "Unknown matching for pixel bit width of: " + bmpBitsPerPixel); } if (bmpCompression != 0) core[0].pixelType = FormatTools.UINT8; MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this); MetadataTools.setDefaultCreationDate(store, id, 0); } // -- Helper methods -- private byte[] uncompress(int no, byte[] buf) throws FormatException, IOException { if (lastImageNo == no) { buf = lastImage; return buf; } CodecOptions options = new CodecOptions(); options.width = getSizeX(); options.height = getSizeY(); options.previousImage = (lastImageNo == no - 1) ? lastImage : null; options.bitsPerSample = bmpBitsPerPixel; options.interleaved = isInterleaved(); options.littleEndian = isLittleEndian(); if (bmpCompression == MSRLE) { byte[] b = new byte[(int) lengths.get(no).longValue()]; in.read(b); MSRLECodec codec = new MSRLECodec(); buf = codec.decompress(b, options); lastImage = buf; lastImageNo = no; } else if (bmpCompression == MS_VIDEO) { MSVideoCodec codec = new MSVideoCodec(); buf = codec.decompress(in, options); lastImage = buf; lastImageNo = no; } else if (bmpCompression == JPEG) { JPEGCodec codec = new JPEGCodec(); buf = codec.decompress(in, options); } /* else if (bmpCompression == CINEPAK) { Object[] options = new Object[2]; options[0] = new Integer(bmpBitsPerPixel); options[1] = lastImage; CinepakCodec codec = new CinepakCodec(); buf = codec.decompress(b, options); lastImage = buf; if (no == core[0].imageCount - 1) lastImage = null; return buf; } */ else { throw new UnsupportedCompressionException( bmpCompression + " not supported"); } return buf; } private void readChunkHeader() throws IOException { readTypeAndSize(); fcc = in.readString(4); } private void readTypeAndSize() throws IOException { type = in.readString(4); size = in.readInt(); } private void readChunk() throws FormatException, IOException { readChunkHeader(); if (type.equals("RIFF")) { if (!fcc.startsWith("AVI")) { throw new FormatException("Sorry, AVI RIFF format not found."); } } else if (in.getFilePointer() == 12) { throw new FormatException("Not an AVI file"); } else { if (in.getFilePointer() + size - 4 <= in.length()) { in.skipBytes(size - 4); } return; } pos = in.getFilePointer(); long spos = pos; LOGGER.info("Searching for image data"); while ((in.length() - in.getFilePointer()) > 4) { listString = in.readString(4); if (listString.equals("RIFF")) { in.seek(in.getFilePointer() - 4); return; } in.seek(pos); if (listString.equals(" JUN")) { in.skipBytes(1); pos++; } if (listString.equals("JUNK")) { readTypeAndSize(); if (type.equals("JUNK")) { in.skipBytes(size); } } else if (listString.equals("LIST")) { spos = in.getFilePointer(); readChunkHeader(); in.seek(spos); if (fcc.equals("hdrl")) { readChunkHeader(); if (type.equals("LIST")) { if (fcc.equals("hdrl")) { readTypeAndSize(); if (type.equals("avih")) { spos = in.getFilePointer(); addGlobalMeta("Microseconds per frame", in.readInt()); addGlobalMeta("Max. bytes per second", in.readInt()); in.skipBytes(8); addGlobalMeta("Total frames", in.readInt()); addGlobalMeta("Initial frames", in.readInt()); in.skipBytes(8); core[0].sizeX = in.readInt(); addGlobalMeta("Frame height", in.readInt()); addGlobalMeta("Scale factor", in.readInt()); addGlobalMeta("Frame rate", in.readInt()); addGlobalMeta("Start time", in.readInt()); addGlobalMeta("Length", in.readInt()); addGlobalMeta("Frame width", getSizeX()); if (spos + size <= in.length()) { in.seek(spos + size); } } } } } else if (fcc.equals("strl")) { long startPos = in.getFilePointer(); long streamSize = size; readChunkHeader(); if (type.equals("LIST")) { if (fcc.equals("strl")) { readTypeAndSize(); if (type.equals("strh")) { spos = in.getFilePointer(); in.skipBytes(40); addGlobalMeta("Stream quality", in.readInt()); bytesPerPlane = in.readInt(); addGlobalMeta("Stream sample size", bytesPerPlane); if (spos + size <= in.length()) { in.seek(spos + size); } } readTypeAndSize(); if (type.equals("strf")) { spos = in.getFilePointer(); if (getSizeY() != 0) { in.skipBytes(size); readTypeAndSize(); while (type.equals("indx")) { in.skipBytes(size); readTypeAndSize(); } pos = in.getFilePointer() - 4; in.seek(pos - 4); continue; } in.skipBytes(4); bmpWidth = in.readInt(); core[0].sizeY = in.readInt(); in.skipBytes(2); bmpBitsPerPixel = in.readShort(); bmpCompression = in.readInt(); in.skipBytes(4); addGlobalMeta("Horizontal resolution", in.readInt()); addGlobalMeta("Vertical resolution", in.readInt()); bmpColorsUsed = in.readInt(); in.skipBytes(4); addGlobalMeta("Bitmap compression value", bmpCompression); addGlobalMeta("Number of colors used", bmpColorsUsed); addGlobalMeta("Bits per pixel", bmpBitsPerPixel); // scan line is padded with zeros to be a multiple of 4 bytes int npad = bmpWidth % 4; if (npad > 0) npad = 4 - npad; bmpScanLineSize = (bmpWidth + npad) * (bmpBitsPerPixel / 8); int bmpActualColorsUsed = 0; if (bmpColorsUsed != 0) { bmpActualColorsUsed = bmpColorsUsed; } else if (bmpBitsPerPixel < 16) { // a value of 0 means we determine this based on the // bits per pixel bmpActualColorsUsed = 1 << bmpBitsPerPixel; bmpColorsUsed = bmpActualColorsUsed; } if (bmpCompression != MSRLE && bmpCompression != 0 && bmpCompression != MS_VIDEO && bmpCompression != JPEG && bmpCompression != Y8) { throw new UnsupportedCompressionException( bmpCompression + " not supported"); } if (!(bmpBitsPerPixel == 4 || bmpBitsPerPixel == 8 || bmpBitsPerPixel == 24 || bmpBitsPerPixel == 16 || bmpBitsPerPixel == 32)) { throw new FormatException(bmpBitsPerPixel + " bits per pixel not supported"); } if (bmpActualColorsUsed != 0) { // read the palette lut = new byte[3][bmpColorsUsed]; for (int i=0; i<bmpColorsUsed; i++) { if (bmpCompression != Y8) { lut[2][i] = in.readByte(); lut[1][i] = in.readByte(); lut[0][i] = in.readByte(); in.skipBytes(1); } else { lut[0][i] = (byte) i; lut[1][i] = (byte) i; lut[2][i] = (byte) i; } } } in.seek(spos + size); } } spos = in.getFilePointer(); readTypeAndSize(); if (type.equals("strd")) { in.skipBytes(size); } else { in.seek(spos); } spos = in.getFilePointer(); readTypeAndSize(); if (type.equals("strn") || type.equals("indx")) { in.skipBytes(size); } else { in.seek(spos); } } if (startPos + streamSize + 8 <= in.length()) { in.seek(startPos + 8 + streamSize); } } else if (fcc.equals("movi")) { readChunkHeader(); if (type.equals("LIST")) { if (fcc.equals("movi")) { spos = in.getFilePointer(); if (spos >= in.length() - 12) break; readChunkHeader(); if (!(type.equals("LIST") && (fcc.equals("rec ") || fcc.equals("movi")))) { in.seek(spos); } spos = in.getFilePointer(); boolean end = false; while (!end) { readTypeAndSize(); String oldType = type; while (type.startsWith("ix") || type.endsWith("tx") || type.equals("JUNK")) { in.skipBytes(size); readTypeAndSize(); } String check = type.substring(2); boolean foundPixels = false; while (check.equals("db") || check.equals("dc") || check.equals("wb")) { foundPixels = true; if (check.startsWith("d")) { if (size > 0 || bmpCompression != 0) { offsets.add(new Long(in.getFilePointer())); lengths.add(new Long(size)); in.skipBytes(size); } } spos = in.getFilePointer(); if (spos + 8 >= in.length()) return; readTypeAndSize(); if (type.equals("JUNK")) { in.skipBytes(size); spos = in.getFilePointer(); if (spos + 8 >= in.length()) return; readTypeAndSize(); } check = type.substring(2); if (check.equals("0d")) { in.seek(spos + 1); readTypeAndSize(); check = type.substring(2); } } in.seek(spos); if (!oldType.startsWith("ix") && !foundPixels) { end = true; } } } } } else { int oldSize = size; size = in.readInt() - 8; if (size > oldSize) { size = oldSize; in.seek(in.getFilePointer() - 4); } // skipping unknown block if (size + 8 >= 0) in.skipBytes(8 + size); } } else { // skipping unknown block readTypeAndSize(); if (in.getFilePointer() + 8 < in.length() && !type.equals("idx1")) { readTypeAndSize(); } else if (!type.equals("idx1")) break; if (in.getFilePointer() + size + 4 <= in.length()) { in.skipBytes(size); } if (type.equals("idx1")) break; } pos = in.getFilePointer(); } } private String getCodecName(final int bmpCompression) { switch (bmpCompression) { case 0: return "Raw (uncompressed)"; case MSRLE: return "Microsoft Run-Length Encoding (MSRLE)"; case MS_VIDEO: return "Microsoft Video (MSV1)"; case JPEG: return "JPEG"; //case CINEPAK: // return "Cinepak"; default: return "Unknown"; } } }